home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
HPAVC
/
HPAVC CD-ROM.iso
/
PCTIM003.ZIP
/
PCTIM003.TXT
< prev
next >
Wrap
Text File
|
1996-02-01
|
644KB
|
14,040 lines
File: PCTIM003.TXT
Description: FAQ / Application notes: Timing on the PC family under DOS
Author: Kris Heidenstrom (kheidens@actrix.gen.nz)
Version: 19951220, Release 3
--------------------------------------------------------------------------------
## 1 INTRODUCTION AND DOCUMENT INFORMATION
## 1.1 DOCUMENT OVERVIEW
This article describes techniques for timing on the IBM PC family under MS-DOS,
and many related subjects. Sample functions and programs are included. After
the brief overview, the features of each technique are listed, so you can find
the most appropriate one for your needs. Subjects covered in this document
include:
■ The DOS and BIOS date/time and alarm functions
■ The BIOS tick count variable
■ Trapping and handling critical errors
■ Using interrupt 1C hex and interrupt 8
■ The counter/timer's internal operation
■ Reprogramming the timer operating mode
■ Measuring short time intervals (three techniques)
■ Reading the timer count in progress
■ Generating an absolute timestamp
■ Reprogramming the timer tick rate
■ Simulating a vertical retrace interrupt for triple buffering
■ Using the serial and parallel port interrupts
■ Reading the joystick position (three methods)
■ Generating tones and sound.
In addition to these timing techniques, this document covers the PC's timing
hardware, and covers interrupts and interrupt considerations in some detail.
Also included in this package is an archive containing executable versions of
the sample programs, and an archive containing six illustrations in GIF format.
## 1.1.1 AUDIENCE
This document is not aimed at programmers who wear suits and write database
query programs in Cobol. It is aimed at the 'tinkerer' programmer or low-level
programmer, who wants complete control of the computer, wants to work closely
with the hardware, and who is familiar with, and interested in, real time
concepts. Previous programming experience in C and assembly language, and
familiarity with DOS and BIOS design, would be an advantage.
## 1.2 CONTENTS
1 INTRODUCTION AND DOCUMENT INFORMATION
1.1 Document Overview
1.1.1 Audience
1.2 Contents
1.3 Author and Distribution
1.4 Disclaimer and Legal stuff
1.5 Document Conventions
1.6 Sample Code Conventions
1.7 Acknowledgements
1.8 Quoter Program
1.9 Revision notes
1.10 Glossary
2 OVERVIEW OF TIMING TECHNIQUES
2.1 The Big Picture
2.2 Which Technique?
2.3 Comparison of Techniques
2.4 Other Subjects Covered in this Document
3 DOS AND BIOS TIME-OF-DAY AND ALARM FUNCTIONS
3.1 Reading the Date and Time from DOS
3.2 Reading the Date and Time from the BIOS
3.3 Sample Program: DOS Device Driver for the AT Clock
3.4 Other BIOS Time and Alarm Functions
3.5 Other Other BIOS Time Functions
3.6 The Times They Are A-Changin'
4 USING THE BIOS TICK COUNT VARIABLE
4.1 The BIOS Tick Count Variable
4.2 Change of Day
4.3 Reading and Setting the Tick Count
4.4 Special Requirements - None
4.5 Sample Program: Reading the Tick Count
4.6 Sample Code: Optimised Function to Read the Tick Count
4.7 Sample Program: Using the Tick Count for Timeout Checking
4.8 Simple Delays using the BIOS Tick Count
5 SPECIAL SOFTWARE PRECAUTIONS
5.1 The Ctrl-C and Ctrl-Break Interrupts
5.2 Handling the Ctrl-C Interrupt
5.3 The Critical Error Interrupt
5.4 Critical Error Handler Parameters
5.5 Critical Error Handler Operation
5.6 The Divide Overflow Interrupt
5.7 Error Handling System
5.8 Sample Code Module: Critical Error Handler module
6 INTERRUPTS
6.1 The Timer Tick Interrupts
6.2 Interrupt Vector Table
6.3 Intercepting an Interrupt
6.4 Interrupt Hardware
6.5 IRQ to Interrupt Mapping
6.6 Interrupt Flag, Interrupt Acceptance, Interrupt Nesting
6.7 EMM386 Interrupt Interception
6.8 Avoiding EMM386 Overhead
6.9 Long Timer Tick Interrupt Handlers
6.9.1 Danger of Long Timer Tick Interrupt Handlers
6.10 Interrupt Mask Register
6.11 Enabling and Disabling the Timer Tick Interrupt
6.12 Reading the Interrupt Request Register
6.13 Reading the Interrupt In Service Register
6.14 When You Should Disable Interrupts
6.15 When You Shouldn't Disable Interrupts
6.16 Causes of Interrupt Delivery Jitter and Fast Tick Loss
6.16.1 Interrupt Delivery Jitter due to Real Interrupts
6.16.2 Interrupt Delivery Jitter due to Software Interrupts
6.16.3 Interrupt Delivery Jitter due to Hardware Accesses
6.16.4 Avoiding Interrupt Delivery Jitter
6.17 Detecting Interrupt Delivery Jitter and Missed Fast Tick Interrupts
6.18 Disabling Interrupts for Longer than One Timer Tick
6.19 Disabling Interrupts for Long Periods of Time
6.20 Overhead of an Interrupt
6.21 Effect of Background Interrupts
6.22 Safe Control of Interrupts
6.23 Timer Tick Interrupt Handler Guidelines
6.24 Accessing Hardware Devices in an Interrupt Handler
6.25 Calling DOS and BIOS in an Interrupt Handler
6.26 Calling C Library Functions in an Interrupt Handler
6.27 Re-entry of Interrupt Handlers
6.28 The 'End Of Interrupt' Signal
6.28.1 Level-Triggered Interrupt Reset
6.29 Enabling and Disabling Interrupts in an Interrupt Handler
6.30 Stack Usage and Stack Checking in an Interrupt Handler
6.31 Chaining to the Old Interrupt Handler
6.32 Writing Interrupt Handlers in Assembly Language
6.32.1 Assembly Language Interrupt Handlers: Accessing Variables
6.32.2 Assembly Language Interrupt Handlers: Starting Condition
6.32.3 Assembly Language Interrupt Handlers: Preserve the Registers
6.33 Using Interrupt Eight in a TSR
6.34 Using int 8 Without Chaining
6.35 Using int 1C hex instead of int 8
6.36 Sample Program: Using int 1Ch With Critical Error and Ctrl-C Handling
6.37 Debugging Interrupt Handlers
7 HARDWARE INFORMATION AND PROGRAMMING
7.1 The 14.31818 MHz Clock
7.2 Clock Frequency Accuracy
7.3 The Counter/Timer Chip (CTC)
7.4 CTC Channels
7.4.1 CTC Channel Zero
7.4.2 CTC Channel Zero Default Operating Mode
7.4.3 CTC Channel One
7.4.4 CTC Channel Two
7.5 Speaker Interface
7.6 CTC Internal Registers
7.7 Access Modes
7.8 CTC Operating Modes
7.8.1 Operating Modes: Behaviour Common to All Modes
7.8.2 Operating Mode Zero: Interrupt on Terminal Count
7.8.3 Operating Mode One: Hardware-Retriggerable One-Shot
7.8.4 Operating Mode Two: Rate Generator
7.8.5 Operating Mode Three: Square Wave Generator
7.8.6 Operating Mode Four: Software-Triggered Strobe
7.8.7 Operating Mode Five: Hardware-Triggered Strobe
7.9 The 8254/8253 Registers
7.9.1 The Mode/Command Register
7.9.2 The Data Ports
7.9.3 Accessing the Registers
7.9.4 I/O Recovery Delays
7.10 Programming the Mode and Reload Register
7.11 Effect of Reprogramming Channel Zero on the Timer Tick Interrupt
7.12 Sample Program: Programming the Mode and Reload Value
7.13 Reading the Reload Register
7.14 Reading the Counting Register
7.15 The Latch Command
7.15.1 Meaning of Count Value in Mode Two
7.15.2 Meaning of Count Value in Mode Three
7.16 Sample Code: Reading the Count in Mode Two
7.17 The Lobyte/Hibyte Flag
7.18 The Read-back Command
7.19 Sample Code: Read-back
7.20 Reading the Count in Mode Three (8254 only)
7.21 Sample Code: Reading the Count in Mode Three
7.22 Sample Code: Optimised Mode Three Count Reading Function
7.23 Sample Program: Manipulate the CTC and Port B
7.24 Hardware Problems and Differences
7.24.1 Differences Between the Intel 8253 and 8254
7.24.2 Chipset Implementations
7.24.3 Intel 8253/8254/82C54 Clock Synchronisation Problems
7.25 Is the CTC an 8253 or an 8254?
7.26 Determining the Exact State of the CTC
7.27 Sample Program: Report Channel States
7.28 CTC Access under OS/2
7.28.1 OS/2 VTIMER.SYS: CTC Channel Zero
7.28.2 OS/2 VTIMER.SYS: CTC Channel One
7.28.3 OS/2 VTIMER.SYS: CTC Channel Two
7.29 Generating Audio Tones on the Speaker
7.30 Sample Program: Generating a Tone using CTC Channel Two
7.31 Timing Short Periods using CTC Channel Two
7.32 Timing Short Periods using Mode Three
7.33 Vertical Retrace
7.34 Sample Program: Timing Short Periods using Mode Three
7.35 The Real Time Clock (RTC)
7.35.1 Reading and Writing RTC Registers
7.35.2 Allocation of the RTC Registers
7.35.3 RTC Register A
7.35.4 RTC Register B
7.35.5 RTC Register C
7.35.6 RTC Register D
7.35.7 Reading the RTC
7.35.8 Sample Program: A TSR Clock using int 8 and the RTC
7.36 The RTC Interrupt and Related BIOS Functions
7.36.1 The BIOS Event Wait and Delay Functions
7.36.2 The BIOS RTC Interrupt Handler
7.36.3 Using the RTC Interrupt
7.36.4 Sample Program: Using the RTC Interrupt
7.37 Using CTC Channel One and Refresh Detect
7.37.1 Sample Program: Timing the Refresh Detect signal
7.37.2 Sample Code: delay(milliseconds) Function using Refresh Detect
8 SPEEDING UP THE TIMER TICK
8.1 The Fast Tick int 8 Handler
8.2 The Interface with the Mainline
8.3 Writing a Fast Tick Handler
8.4 Comments on Fast Timer Tick Interrupts
8.5 Sample Program: Morse Player using Fast Timer Tick
8.6 Dynamic Fast Tick Periods
8.7 Sample Program: Dynamic Fast Tick Interrupt Handler
9 READING AN ABSOLUTE TIMESTAMP
9.1 Sample Program: Absolute Time Reference (Timestamp) in Mode Two
9.2 Sample Program: Absolute Timestamp in Mode Two - Assembler
9.3 Handling the Midnight Boundary
10 OTHER TOPICS
10.1 The 586 Time Stamp Counter
10.2 Serial Port Regular Interrupt
10.2.1 Serial Port (UART) Documentation
10.2.2 Sample Program: Regular Interrupt using the Serial Port
10.2.3 Inserting Delays into Serial Port Transmitted Data
10.3 External Interrupt Sources
10.3.1 External Interrupt through Parallel Port
10.3.2 External Interrupt through Serial Port
10.3.3 External Interrupt through Sound Card
10.3.4 External Interrupt through Custom I/O Card
10.4 The Joystick Port
10.4.1 Joystick Port Hardware
10.4.2 Reading the Joystick Buttons and Position
10.4.3 Notes from the PC-GPE Article
10.4.4 Sample Program: Reading the Joystick Position
10.4.5 Using the Joystick Port for General Purpose Input
10.4.6 Joystick Left/Right and Up/Down Detection
10.5 The Mouse and Mouse Driver [not written]
10.6 Networks
10.7 Sound Generation
10.7.1 Pulse Width Modulation (PWM) Principle
10.7.2 PWM Audio Generation Implementation
10.7.3 Sample Program: DTMF Generation using PWM
10.7.3.1 Sample Program Explanation
10.7.3.2 Other Methods of Sound Generation
10.7.4 Peter Moylan's MUSIC Package
10.8 Related Software Packages
10.8.1 The ATIM Package
10.8.2 The MSCHRT and TCHRT Packages
10.8.3 The TCTIMER Package
10.8.4 The MILLISEC Package
10.8.5 The MSEC_12 Package
10.8.6 The ERTIMER Package
10.8.7 The FASTCLOK Package
10.9 Benchmarking Considerations
10.10 Granularity and Uncertainty
10.11 Converting between Microseconds and CTC Clocks
10.12 Maintaining a Millisecond or Microsecond Count
10.12.1 Sample Program: Millisecond Count using int 1Ch
10.13 Notes on Microsoft Windows
10.14 DOS File Date and Time Stamps
10.15 DOS and the Date and Time
10.15.1 DOS Date Rollover Bugs
10.16 Simulating a Vertical Retrace Interrupt
10.16.1 Vertical Retrace Interrupt Simulation Description
10.16.1.1 Measuring the Field Time
10.16.1.2 Controlling the CTC Interrupt
10.16.1.3 Significance of the SafeMargin Value
10.16.1.4 Overhead due to Large SafeMargin and Screen Update
10.16.1.5 Enhanced Handling of Missed Retrace Start
10.16.1.6 Other Notes
10.16.2 Sample Program: Simulating a Vertical Retrace Interrupt
10.16.3 Triple Buffering
11 QUESTIONS AND ANSWERS
11.1 Timing Accuracy
11.2 Timer Interrupts (int 8, int 1Ch, RTC Interrupt)
11.3 Interrupt Priorities and Nesting
11.4 Interrupt Handler Restrictions
11.5 High Speed Timer Tick
11.6 DOS Date and Time
11.7 Accessing Hardware
11.8 Miscellaneous
12 REFERENCES
## 1.3 AUTHOR AND DISTRIBUTION
This document (including sample code and programs) is Copyright (c) 1994-1996
by K. Heidenstrom. Please send corrections/additions/comments/suggestions to:
Email: kheidens@actrix.gen.nz
Snail mail: K. Heidenstrom, c/- P.O. Box 27-103, Wellington, New Zealand.
If you send me comments, corrections etc via email or on a disk, you may find
the quoter program described in section »» 1.8 helpful. It will generate a
quoted copy of this file, to help you with marking up the document with your
comments.
The archive may be freely distributed via any electronic medium provided that
it is not modified in any way, and that no charge (other than the normal charge
to cover the disk, CD, etc) is made.
The sample code and sample programs may be freely used in any commercial or
non-commercial software.
If you find this document useful, I would appreciate a postcard, or an email
message, especially if you tell me a bit about your project.
I'm pretty sure of this stuff, and I've done a bit of research (not as much as
I should have done :-), but don't take it all as gospel. I have had to work
some things out by myself and I may have got something wrong. If you know
better about anything in here, please please drop me a message, so that other
readers of this document can benefit from your experience. Thanks!
FILE_ID.DIZ contents and SimTel information:
pctim003.zip FAQ / App notes: Timing on the PC under DOS
This archive contains a technical document useful to PC programmers,
with many sample programs. The document covers timing and related
subjects on the IBM PC family under DOS. Subjects include BIOS and
DOS functions, the BIOS tick count, hardware interrupts, timer tick
interrupts, Port B, the 8253/8254 timer, speeding up the timer tick,
dynamic tick periods, simulated vertical retrace interrupt, double
and triple buffering, absolute timestamping, the RTC, other timing
methods, reading the joystick, PWM sound generation. Freeware.
13400 lines, PC ASCII, 340K ZIP file. Release 3, February 1996.
Author: Kris Heidenstrom, kheidens@actrix.gen.nz.
Simtel directory: SimTel/msdos/info/
Keywords:
145818 8253 8254 8255 8259 AT B CTC BIOS Delay DOS I/O IBM Interrupt
Joystick MS-DOS PC PIC PIT PWM Port RTC Tick Timestamping Timing
This document should be named PCTIMxxx.TXT where xxx is the release number
shown at the top of the file. The latest version will always be available
on SimTel (ftp.coast.net), or mirrors (such as Oakland). The file's URL at
SimTel is ftp://ftp.coast.net/SimTel/msdos/info/pctim*.zip.
Your browser may not accept a wildcard specification (i.e. the asterisk), and
may say that the file does not exist. If so, view a listing of the SimTel/
msdos/info directory, find the file name, and modify the URL accordingly.
## 1.4 DISCLAIMER AND LEGAL STUFF
I make no warranty of any kind with regard to this information and sample code.
In no event shall I be liable for any damages whatsoever for any loss involving
the use of this information or sample code, or due to any errors or omissions.
Trademarks and service marks mentioned in this document are the property of
their respective owners. Most of them probably know who they are :-)
## 1.5 DOCUMENT CONVENTIONS
This file is formatted for viewing on an IBM or compatible (American ASCII
with high-ASCII box characters, i.e. codepage 437) with an 80-column monospaced
(i.e. text-mode) display, using tab stops every 8 columns. I have designed the
document to work with DOS file viewers such as Vern Buerg's famous LIST program.
Sections are hierarchically numbered. The contents is near the start of the
file, and each section or subsection is announced by two '#' characters, a
space, and the section number, to facilitate searching. I have mostly used
British spelling.
There are six illustrations in GIF format, which are enclosed in the FIGURES
archive. Since they are line drawings, they do not look good if rescaled, so
try to view them at their original resolutions if possible.
Currently only the plain ASCII text version, in English, exists. There does
not seem to be a good widely-used alternative at the moment. I would try Tex
but I don't have a spare hard drive and six spare months to figure out how to
use it! Let me know if you would like a Word Perfect 6.0 (DOS) or Word Perfect
for Windows version and if there is enough interest I may create one. Also if
you want to create an HTML version of this document, please get in touch!
Numbers are decimal unless indicated. Hex is indicated by '0x' prefix or 'h'
suffix, e.g. 0x55AA, 1Ch.
Throughout this document, I refer to the 8253/8254 timer chip as the 'CTC'
(counter/timer chip, or counter/timer circuit). This term is not normally used
for this particular chip. Intel calls it the PIT (programmable interval timer).
I mention this because you may get corrected if you publically call it the CTC.
I have had a great deal of trouble maintaining a logical organisation in this
document. I welcome any suggestions for improving its readability and
understandability :-)
Some subjects are outside my experience and I have marked these with (*).
If you can fill in any of these gaps, this would be much appreciated.
## 1.6 SAMPLE CODE CONVENTIONS
The sample code is in C and assembler, but you could convert it to Pascal or
convert the C code to assembler. In most cases, I have aimed to be instructive
rather than highly optimal. The sample programs are starting points - they are
complete stand-alone programs, but are not necessarily very useful. They have
been briefly tested with Borland C++ 2.0, Borland TASM 3.1, and Borland TLINK
version 4.0. Short sample functions are untested. Let me know if you have any
trouble with them.
I have used small model for the C programs, so code and data are near, but this
could be changed easily. The assembly language programs are in tiny model and
should assemble with either MASM or TASM; I have had to forgo TASM's Ideal Mode
and all of my nice macros. :-(
I have listed #defines in each sample program as required. When I have re-used
already-documented functions I have kept the name and coding the same, but have
removed the comments from all but the first occurrence of the function.
MS-DOS (version 2.0 or later) or a compatible operating system is assumed.
## 1.7 ACKNOWLEDGEMENTS
My thanks for suggestions, information, criticism, and/or encouragement, to:
Michael Bishop mxbish2@lookout.ecte.uswc.uswest.com
Gordon Burditt gordon@sneaky.lonestar.org
Jan-Pieter Cornet cornet@duteca2.et.tudelft.nl
Saul Cozens s.cozens@sheffield.ac.uk
David Empson dempson@actrix.gen.nz
Klaus Hartnegg klaus@mailserv.brain.uni-freiburg.de
Gian Uberto Lauri saint@dei.unipd.it
William Luitje luitje@m-net.arbornet.org
Terje Mathisen Terje.Mathisen@hda.hydro.com
Michael Mauch mauch@uni-duisburg.de
John Mertus mertus@brownvm.brown.edu {JAM}
Peter Moylan peter@fourier.newcastle.edu.au
Anders Roar Nielsen aroni@night.ping.dk
Philip O'Carroll poc@maths.tcd.ie {POC}
James Ralph jim@grc.com
Paul Ross pa-ross@uwe.ac.uk
Tor Sjowall tor@oslonett.no {TOR}
Bob Smith bobs@access.digex.net
John Stockton jrs@dclf.npl.co.uk
Louis Warshaw louis@gate.net
Please tell me if your name should be on this list!
To give credit where it is due, throughout the text I have flagged specific
contributions with the names shown in squiggly brackets.
In particular, I have used (with permission) information about sampled audio
generation on the PC speaker from a PC speaker music package written by Peter
Moylan with help from Tim Channon. The technique mentioned here was also
described by Mark Feldman (the PC-GPE guru). See section »» 10.7.
I have also used many invaluable pieces of information (again with permission)
from a collection of papers by Prof. John Mertus. Prof. Mertus's papers deal
with subject testing (e.g. reaction timing), timer accuracy, and statistical
analysis techniques for validating correct and reliable performance on various
machines in various configurations (e.g. in protected mode, or on networked
machines) which I have not covered in this document. They are thorough, and
very interesting. You can FTP his files, in PostScript and LaTeX formats,
from: ftp://jam.cog.brown.edu/pub/timing/ (various files).
I have paraphrased his comments to maintain continuity in my document, and used
the marker {JAM} so that credit goes where it is due. Any mistakes in the
interpretation are mine, however. Prof. John Mertus owns the copyright on the
above mentioned documents, please respect the considerable amount of work which
has gone into them, by giving him credit if you use them.
## 1.8 QUOTER PROGRAM
To generate a quoted version of this file so you can report problems to me, I
have included in the SAMPLES archive a small program called QUOTE.COM, which
operates as a quoting filter. Entering "C:\> QUOTE <PCTIMxxx.TXT >QUOTED.TXT"
(where 'xxx' is the release number) will generate a quoted copy of this document
for you to edit and mark up. You will probably want to use an editor that can
handle more than 80 columns when editing the quoted copy.
## 1.9 REVISION NOTES
Release 1 19950417
Release 2 19950816
Release 3 19960201
This is the third release of this document. At this point, I have at last
covered all the important timing-related subjects that I know about.
If you would like to see any other subjects covered, or would like to submit
documentation or code on other relevant subjects, please get in touch.
Otherwise the only intended future changes will be for correctness and to
resolve the items indicated with (*) if possible.
Changes from release 2 to release 3:
■ Added information and sample program for vertical retrace interrupt simulation
■ Tidying up
■ Improved comparison of techniques
■ Various improvements suggested by Dr. John Stockton
■ Important note relating to long timer tick interrupt handlers added,
see section »» 6.9.1.
■ Added questions and answers section
■ Added six illustrations in GIF format (hoping CI$ don't sue me :-)
■ Added discussion on int 8 versus int 1Ch
■ Added info on the triple buffering technique that can be used in
conjunction with vertical retrace interrupt simulation
■ Brief mentions of Microchannel int 8 reset
■ Brief description of joystick left/right and up/down under interrupt
■ Several notes from Michael Mauch (mauch@uni-duisburg.de) included
■ Much expanded explanation of I/O access and recovery delays (section
»» 7.9.4)
■ Version 1.1.0 of quoter program, proper tab handling
Changes from release 1 to release 2:
■ Added sample program to read and write CTC registers and Port B with a
command-oriented interface
■ Added information on timing-related software packages
■ Added brief notes on benchmarking considerations
■ Modified sample code for short period timing using channel 2 to generate a
strobe pulse on the parallel port with a duration of 5 us plus overhead
■ Added code for converting between microseconds and CTC clocks
■ Corekkted twleve typoes
■ Fixed various minor clumsy explanations and stupid mistakes
■ Added notes on Windows considerations from {TOR}
■ Added description and sample function for handling midnight boundary
when calculating elapsed time from absolute timestamp values
■ Added timing using Refresh Detect signal on Port B (thanks to William Luitje)
■ Added sample code to determine keyboard interface type (PC/XT or AT and later)
■ Documentation on resolution and uncertainty
■ Documentation and sample program for millisecond count variable
■ Added delay(milliseconds) function using Refresh Detect
■ Added Refresh Detect method of reading the joystick position
■ Added notes on generating delays in serially transmitted data
■ Added sample program to generate DTMF using PWM audio techniques
■ Added information on DOS internal handling of date and time
■ Include sample programs in executable form in the distribution file
## 1.10 GLOSSARY
ASIC
Application Specific Integrated Circuit, a high density custom chip.
BCD
Binary Coded Decimal, an encoding scheme where each digit of a decimal
number is represented by four adjacent bits in a register. For example
in BCD the number ninety-seven would be represented by 10010111 binary.
The binary representation of 97 is 01100001.
BIOS
Basic Input/Output System, software in ROM chips on the motherboard.
Bit
If you don't know what a bit is, you are reading the wrong document :-)
Channel
One of three independent counting or timing circuits in the CTC. Also
referred to as a 'timer'.
Clock
[n] An electrical signal at a fixed frequency [in this context].
[v] To trigger to perform a certain action. For an electrical clock,
the action is performed at the instant of the rising or falling
edge of the clock signal.
Count
[n] The value in a counter at a given moment in time.
[v] What it usually means :-)
Counter
A register which increments or decrements when clocked.
Counting register
The counter in a CTC channel. It decrements when clocked, and can be
reloaded from the Reload register. See section »» 7.3
CTC
Counter/Timer Chip (or Circuit), the 8253 (PC, XT) or 8254 (AT and
later) chip or functional equivalent. I prefer the term 'CTC' and
use it in this document, but the CTC is more commonly known as the
'Timer', the 'Counter', and the 'PIT' (Programmable Interval Timer),
which is Intel's name for the chip.
CTC clock
The clock input frequency to the CTC, 1.193181666666... MHz.
Decrement
Count down (usually by 1).
Divide [frequency]
To generate a lower frequency from a higher frequency by counting
pulses and producing an output pulse when a certain number of input
pulses have occurred.
Divisor register
Another name for the Reload register when modes 2 or 3 are used.
See section »» 7.3.
DMA
Direct Memory Access, a technique where hardware (e.g. a floppy disk
drive adapter or sound card) transfers data directly to or from memory,
without processor intervention.
EISA
Enhanced Industry Standard Architecture, the bus structure used in some
more modern PCs. It is an extension of the ISA architecture.
EOI
End of Interrupt, a command to the PIC to indicate that an interrupt
handler has completed, see section »» 6.28.
Flag
A single bit indicating yes/no, true/false, on/off, enabled/disabled,
or any condition which has two possible (and usually opposite) states.
Frequency
How often something occurs, per second. 18.2065 Hz (hertz) means
18.2065 times per second.
Hz
Hertz, the unit of frequency.
IMR
Interrupt Mask Register in the PIC.
Increment
Count up (usually by 1).
Interrupt
[n] A hardware- or software-generated interruption to the processor.
[v] To suspend processing and cause the processor to execute a special
section of code (the interrupt handler).
Interrupt Controller
See PIC.
Interrupt Handler
See Interrupt Service Routine.
Interrupt Service Routine
A section of code which is executed in response to an interrupt which
'services' (attends to) the hardware device or software invocation
which generated the interrupt.
Interrupt Vector
See Vector.
IRQ
Interrupt request, a hardware interrupt source, handled by the PIC(s).
IRR
Interrupt Request Register, part of the 8259 PIC, see section »» 6.12.
ISA
Industry Standard Architecture (Also Irritatingly Slow Architecture),
the bus structure of the PC, XT, and AT. Contrast to EISA, MCA and PCI
architectures. Despite its limitations, it is still the most common bus
structure. Many of these limitations are avoided with the VESA Local
Bus extension.
ISR
Interrupt Service Routine. Also In Service Register, section »» 6.13.
IVT
Interrupt Vector Table, a table of 256 interrupt vectors occupying the
first 1024 bytes of physical memory (in real and 8086 emulation modes).
{JAM}
See section »» 1.7.
Jitter
Unevenness, inconsistency, fluctuation, variation, or irregularity.
LSI
Large Scale Integration, a high density chip, see ASIC
MCA
Microchannel Architecture, the bus structure used in most IBM PS/2
machines. Sort of a dead duck as far as architectures are concerned.
MHz
Megahertz, one million hertz.
Mode
Of a CTC channel, the operational algorithm, or definition of behaviour,
which has been selected (programmed) for that channel.
Monostable
A circuit which has one stable state (in which it will remain until
triggered externally) and one unstable state (in which it will remain
for a given period of time). Also called a one-shot. When triggered,
it switches to its unstable state, and after a period of time, it
returns to its stable state until triggered again.
ms
Millisecond(s), one thousandth of a second.
NMI
Non-Maskable Interrupt, an emergency interrupt source that cannot be
masked (cannot be disabled under software control).
PIC
Programmable Interrupt Controller, an Intel 8259 chip or functional
equivalent, which arbitrates IRQs and issues hardware interrupt
requests to the processor. The PC and XT have one PIC, the AT has two.
See section »» 6.4.
{POC}
See section »» 1.7.
Port
A link between software and hardware. Allows software to 'talk' to
hardware devices. Also a connector on the back of the PC (e.g. serial
or parallel port).
POST
Power-On Self-Test, the initialisation and test functions of the BIOS.
PPI
Programmable Peripheral Interface, an Intel 8255, used on the PC and XT,
replaced by the keyboard controller on the AT and later machines.
ppm
Parts Per Million. 10000 ppm is one percent. 1 ppm is 0.0001 percent.
1 ppm corresponds to 0.0864 seconds per day; 11.5741 ppm is one second
per day.
Prefetch queue
A look-ahead buffer in the processor which 'pre-fetches' instructions
ahead of the current execution point during gaps when memory is not
being accessed (i.e. while instructions are being internally processed
by the processor) so that the instructions are ready before they are
needed. This method is based on the assumption that instructions are
executed in sequence. A jump, call, return, interrupt, or conditional
branch instruction (if the branch is taken) disrupt this sequence and
cause the prefetch queue to be flushed, slowing execution.
Processor
The Intel 80x86 central processing unit or functional equivalent.
Reload register
Register which contains the value which is reloaded into the Counting
register under certain circumstances (depending on the mode), see
section »» 7.3.
Register
A group of bits, can be used to store and manipulate numbers.
ROM
Read-Only Memory, a chip containing factory programmed software.
RTC
Real Time Clock, also called RTC/RAM or CMOS. A Motorola MC146818
or workalike, containing real-time date and time registers and
battery-backed-up storage for BIOS parameters (CMOS).
Tick
The timer interrupt which normally occurs 18.2065 times per second.
Timer
See 'Channel' and 'CTC'.
TLA
Itself
{TOR}
See section »» 1.7.
TSR
Terminate and Stay Resident, a memory-resident pop-up utility program.
UART
Universal Asynchronous Receiver/Transmitter; a chip which transmits and
receives asynchronous serial data (e.g. to a modem). The UART used in
the PC is the 8250 or one of its descendants.
us
Microsecond(s), one millionth of a second.
Vector
[n] A pointer to a section of code, often an interrupt service routine.
[v] To execute the code pointed to by a vector.
VGA
A video adapter standard. It is the basic standard for most current
video hardware. The name comes from Video Graphics Array, the ASIC
that implements the video hardware in the PS/2.
-WR
An active low write signal. The '-' prefix means active low. When
this line goes low, the processor is writing data into a peripheral.
## 2 OVERVIEW OF TIMING TECHNIQUES
This section gives you the big picture, then presents the timing techniques
that will be described in detail in later sections, so you can choose the
technique that interests you.
## 2.1 THE BIG PICTURE
Figure 1 (in the accompanying FIGURES archive) gives a general overview of the
two main timing subsystems in the PC, and their interfaces to the processor.
The 14.31818 MHz system clock is divided by 12 to give a 1.193182 MHz clock
(period is 0.8381 microseconds) which clocks the three channels of the 8253/8254
counter/timer chip (CTC). The CTC divides this frequency to lower frequencies
using programmable divisors, and produces three output signals.
CTC channel zero's output is connected directly to IRQ0 on the primary PIC (8259
interrupt controller chip), and generates int 8, the timer tick interrupt, about
18.2065 times per second, or once every 54.9254 milliseconds. The timer tick is
a regular interrupt which allows certain actions (such as updating the system
time-of-day) to be executed periodically.
Interrupt 8 is serviced by the ROM-BIOS. The BIOS's int 8 handler increments
the BIOS tick count variable (a 32-bit variable used for timekeeping) and turns
off the floppy disk drive motors two seconds after they were last accessed. It
also issues int 1C hex, which may be used as a regular interrupt source by user
programs.
The BIOS tick count is a 32-bit counter at low memory address 0040:006C, which
contains the number of timer ticks (units of 54.9254 ms) since midnight and is
used by DOS to calculate the time of day.
CTC channels 1 and 2 can also be used for timing, via the Refresh Detect and
Timer 2 readback signals on Port B. Channel 2 also generates audio for the
PC speaker, and can be used in conjunction with channel 0 for PWM audio
generation.
The CTC divides its 1.193182 MHz clock down to 18.2065 Hz using a 16-bit
counter. It is possible to read the actual count in progress in the CTC.
In combination with the tick count variable, this can give an absolute time
value, in units of 0.8381 us, for timestamping, elapsed time calculation, etc.
In some applications, a timer tick rate faster than 18.2065 times per second is
required. This can be achieved by reprogramming the CTC. The CTC is told to
generate the timer tick at a faster rate, and the program intercepts the timer
tick interrupt (int 8). The int 8 handler does its thing, and calls the old
int 8 handler at the correct rate (18.2065 times per second) to maintain the
correct system time.
The Real Time Clock (RTC) was introduced with the AT, and all hardware-
compatible ATs and later machines have one. The RTC is completely independent
of the CTC. It uses a 32.768 kHz watch crystal for timekeeping and is battery
backed up (i.e. continues to keep time while the computer is powered off).
It can be used to generate a periodic interrupt, usually at 1024 Hz (1024
interrupts per second).
## 2.2 WHICH TECHNIQUE?
There are three basic approaches to timing. Often two approaches can be used
together. The techniques are summarised and compared in section »» 2.3.
■ ABSOLUTE TIME REFERENCE
You can write a function for use by your program that returns a value
representing the absolute time, with units and resolution of one tick
(54.9254 ms), or 977 us (the RTC regular interrupt rate), or one CTC
clock (0.8381 us).
■ RELATIVE TIME REFERENCE
Your program can use the CTC to measure short time durations, for
example to generate a short pulse on an I/O port pin or measure an
external signal.
■ REGULAR INTERRUPT
An interrupt handler is called at regular (or sometimes, irregular)
intervals, e.g. the default rate of once every 54.9254 ms, or 1024
times per second using the RTC, or at a user-selectable rate if you
reprogram the CTC. The interrupt handler can perform operations in
the background and/or maintain an absolute time variable.
## 2.3 COMPARISON OF TECHNIQUES
'Special precautions' in the following table refers to intercepting the DOS
Ctrl-C, Critical Error, and Divide Overflow vectors so that interrupt vectors
and/or hardware states can be restored safely when the program is terminated
(see section »» 5 and subsections).
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Call DOS to read time-of-day │
│ Type: Absolute time reference │
│ Resolution: 55 ms or one second │
│ Special precautions: Not required │
│ Use in TSRs: Not without special TSR techniques │
│ Works under OS/2: Yes │
│ Notes: Portable to all DOS and DOS compatible systems │
│ Applications: Low resolution, absolute time value │
│ Described in: Section »» 3.1 │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Call BIOS RTC functions to read time-of-day │
│ Type: Absolute time reference │
│ Resolution: One second │
│ Special precautions: Not required │
│ Use in TSRs: Usually safe │
│ Works under OS/2: Yes │
│ Applications: Low resolution, absolute time value │
│ Described in: Section »» 3.2 │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Read RTC time of day directly │
│ Type: Absolute time reference │
│ Resolution: One second │
│ Special precautions: Not required │
│ Use in TSRs: Yes │
│ Works under OS/2: Probably │
│ Applications: Low resolution, absolute time value │
│ Described in: Section »» 7.35 and subsections │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Use the BIOS tick count variable │
│ Type: Absolute time reference │
│ Resolution: 55 ms │
│ Special precautions: Not required │
│ Use in TSRs: Yes │
│ Works under OS/2: Yes │
│ Notes: Can be read from within an interrupt routine │
│ Applications: General absolute time value, low resolution │
│ Described in: Section »» 4 and subsections │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Use int 1C hex │
│ Type: Regular interrupt │
│ Resolution: 55 ms │
│ Special precautions: Required │
│ Use in TSRs: No (see section »» 6.35) │
│ Works under OS/2: Yes │
│ Applications: Low resolution regular interrupt │
│ Described in: Section »» 6.1, section »» 6.35 │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Intercept int 8 (in TSRs) │
│ Type: Regular interrupt │
│ Resolution: 55 ms │
│ Special precautions: Not required if used in a TSR │
│ Use in TSRs: Yes │
│ Works under OS/2: Yes │
│ Applications: Regular interrupt for timing and/or popup by TSRs │
│ Described in: Section »» 6.33 │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Read CTC channel 0 on-the-fly in mode two │
│ Type: Absolute timestamp │
│ Resolution: 0.8381 us │
│ Special precautions: Not required │
│ Use in TSRs: Yes │
│ Works under OS/2: Only if HW_TIMER = ON │
│ Notes: Can be read from within an interrupt routine │
│ Applications: Absolute time value, high resolution │
│ Described in: Section »» 7.16 and section »» 9 and subsections │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Read CTC channel 0 on-the-fly in mode three │
│ Type: Absolute timestamp │
│ Resolution: 0.8381 us │
│ Special precautions: Not required │
│ Use in TSRs: Yes │
│ Works under OS/2: Only if HW_TIMER = ON │
│ Notes: Can be read from within an interrupt routine │
│ Will not work on a PC, XT, or PS/2 │
│ No advantages over using mode two │
│ Applications: Absolute time value, high resolution │
│ Described in: Section »» 7.20, section »» 7.21, section »» 7.22 │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Use CTC channel 2 for timing short delays │
│ Type: Relative time reference │
│ Resolution: 0.8381 us │
│ Special precautions: Not required │
│ Use in TSRs: Yes │
│ Works under OS/2: Only if HW_TIMER = ON │
│ Notes: Can be used within an interrupt routine │
│ Good for implementing short timeouts │
│ Should only be used with interrupts locked out │
│ Disrupts the system beep if used under interrupt │
│ Applications: Short delays, useful in dedicated hardware control │
│ Described in: Section »» 7.31, section »» 10.4.4 │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Read CTC channel 0 in mode three for short delays │
│ Type: Relative time reference │
│ Resolution: 0.8381 us │
│ Special precautions: Not required │
│ Use in TSRs: Yes │
│ Works under OS/2: Only if HW_TIMER = ON │
│ Notes: No advantages over using mode two │
│ Applications: Short delays, useful in dedicated hardware control │
│ Described in: Section »» 7.32 │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Vertical Retrace (polled) │
│ Type: Relative time reference │
│ Resolution: Medium (1/60 or 1/72 of a second) │
│ Special precautions: Not required │
│ Use in TSRs: Yes │
│ Works under OS/2: Probably not │
│ Notes: Useful for synchronising to screen scan │
│ Applications: Screen scan synchronisation in games, graphics apps │
│ Described in: Section »» 7.33 │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: RTC Periodic Interrupt │
│ Type: Regular interrupt │
│ Resolution: 976.5625 us │
│ Special precautions: Required │
│ Use in TSRs: Not really safe │
│ Works under OS/2: Probably not │
│ Notes: Doesn't interfere with the CTC │
│ Convenient resolution │
│ Won't work on PCs and XTs │
│ Applications: Programs that slow the machine or time other programs │
│ Described in: Section »» 7.36 and subsections │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: BIOS Delay and Event Wait functions │
│ Type: Relative delay │
│ Resolution: 976.5625 us │
│ Special precautions: May be required │
│ Use in TSRs: Not safe │
│ Works under OS/2: Probably not │
│ Notes: Doesn't interfere with the CTC │
│ Won't work on PCs and XTs │
│ Applications: General delays or timeouts with about 1ms resolution │
│ Described in: Section »» 7.36.1 │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Refresh Detect (CTC channel 1 read-back) │
│ Type: Relative time reference │
│ Resolution: 15.0857 us │
│ Special precautions: Not required │
│ Use in TSRs: Yes │
│ Works under OS/2: No │
│ Notes: High resolution │
│ Very tidy way to generate short delays │
│ Can be used to generate delays of 'at least x' with │
│ interrupts enabled │
│ Can be used within an interrupt routine │
│ Interrupts, if enabled, will lengthen the delay │
│ Won't work if the RAM refresh rate has been changed │
│ Won't work on old PCs and XTs │
│ Applications: Short delays, timeouts, timing input signals │
│ Described in: Section »» 7.37 │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Speed up CTC channel 0 (timer tick) rate │
│ Type: Regular or irregular interrupt │
│ Resolution: Settable │
│ Special precautions: Required │
│ Use in TSRs: No │
│ Works under OS/2: Only if HW_TIMER = ON │
│ Notes: Can generate exact interrupt rate (e.g. 500us, 1ms) │
│ May affect other DOS sessions under OS/2 with HW_TIMER │
│ Applications: Fast regular interrupt source - used for games, etc │
│ Described in: Section »» 8 and subsections │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Intel 586 Time Stamp Counter │
│ Type: Absolute or relative time reference │
│ Resolution: Extremely high │
│ Special precautions: Not required │
│ Use in TSRs: Yes │
│ Works under OS/2: Probably │
│ Notes: Ridiculously high resolution │
│ Disadvantages: Doesn't work on 486 or lower │
│ Not guaranteed to work on future processors │
│ Timing unit depends on processor clock speed │
│ Applications: High resolution timestamping for usage billing │
│ Described in: Section »» 10.1 and subsections │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: Regular interrupt from serial port │
│ Type: Regular interrupt │
│ Resolution: Selectable │
│ Special precautions: Required │
│ Use in TSRs: Not reliably │
│ Works under OS/2: No │
│ Notes: User-selectable interrupt rate │
│ Doesn't affect the CTC or the RTC │
│ Requires a spare serial port │
│ Applications: Slow or fast regular interrupt │
│ Described in: Section »» 10.2 and subsections │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Technique: External regular or irregular interrupt source │
│ Type: Regular or irregular interrupt │
│ Resolution: Depends on external hardware │
│ Special precautions: Required │
│ Use in TSRs: May not be reliable │
│ Works under OS/2: Probably not │
│ Notes: Can be very versatile │
│ Requires special hardware │
│ Applications: Slow or fast interrupt using special hardware │
│ Described in: Section »» 10.3 and subsections │
└──────────────────────────────────────────────────────────────────────────────┘
## 2.4 OTHER SUBJECTS COVERED IN THIS DOCUMENT
I've included, in addition to timing related documentation, info on handling
the DOS Ctrl-C, critical error, and divide overflow interrupts (required if
you are going to intercept any other interrupts), see section »» 5 and
subsections, lots of general information about interrupts, information on
various relevant hardware devices, information on the joystick hardware,
information on sound and music generation using a technique called PWM (see
section »» 10.7), and information on vertical retrace interrupt emulation
(section »» 10.16).
## 3 DOS AND BIOS TIME-OF-DAY AND ALARM FUNCTIONS
In high level languages, library functions are available to get the time of day
and should be used for portability. Internally they use the DOS time of day
functions. Assembly language programmers can use the DOS and BIOS functions
directly.
## 3.1 READING THE DATE AND TIME FROM DOS
DOS functions 2A, 2B, 2C, and 2D hex relate to time of day. To use them, set
AH to the function number, and set other registers as applicable, and issue int
21 hex. All values are accepted and returned in binary form (i.e. not BCD).
Get Date : DOS functions (int 21h)
Call with: AH = 2A hex
Returns: AL = Day of week (0 to 6 correspond to Sun to Sat)
CX = Year in full (1980 to 2099, 7BCh to 833h)
DL = Day of month (1 to 31)
DH = Month of year (1 to 12 correspond to Jan to Dec)
Get Time : DOS functions (int 21h)
Call with: AH = 2C hex
Returns: CH = Hours (0 to 23, using 24-hour clock format)
CL = Minutes (0 to 59)
DH = Seconds (0 to 59)
DL = Hundredths of seconds (0 to 99) (see note below)
Set Date : DOS functions (int 21h)
Call with: AH = 2B hex
CX = Year in full (must be 1980 to 2099)
DL = Day of month (1 to 31, depending on month)
DH = Month of year (1 to 12)
Returns: AL = Success/failure: 0 = OK, 0FFh = Bad date specified
Set Time : DOS functions (int 21h)
Call with: AH = 2D hex
CH = Hours (0 to 23, 24-hour clock format)
CL = Minutes (0 to 59)
DH = Seconds (0 to 59)
DL = Hundredths of seconds (0 to 99) (see note below)
Returns: AL = Success/failure: 0 = OK, 0FFh = Bad time specified
The time of day is calculated from the BIOS tick count variable. The hundredths
of seconds value is approximated using an internal algorithm which apparently
produces an even distribution of values, but its resolution is only as good as
the tick counter, i.e. 54.9254 ms. See section »» 10.15 for more information.
## 3.2 READING THE DATE AND TIME FROM THE BIOS
BIOS functions provide access to the tick count and the RTC (Real-Time Clock),
accessed by issuing int 1A hex. (The BIOS tick count functions are also part
of this interrupt, but should not be used - see section »» 4.3 for details).
The RTC functions accept and return values in BCD form.
The RTC functions are present on the AT and all later machines, but not on
the original PC or XT (there may be some hybrid machines that do support them,
but I don't know of any).
Get RTC Date : int 1Ah
Call with: AH = 04 hex
Returns: CH = Hundreds of years (19h or 20h, BCD format)
CL = Year (00h to 99h, BCD format)
DH = Month (01h to 12h, BCD format)
DL = Day of month (01h to 31h, BCD format)
CF = Error status, carry is set if clock is not running
Get RTC Time : int 1Ah
Call with: AH = 02 hex
Returns: CH = Hours (00h to 23h, BCD format)
CL = Minutes (00h to 59h, BCD format)
DH = Seconds (00h to 59h, BCD format)
CF = Error status, carry is set if clock is not running
Set RTC Date : int 1Ah
Call with: AH = 05 hex
CH = Hundreds of years (19h or 20h, BCD format)
CL = Year (00h to 99h, BCD format)
DH = Month (01h to 12h, BCD format)
DL = Day of month (01h to 31h, BCD format)
Returns: Nothing
Set RTC Time : int 1Ah
Call with: AH = 03 hex
CH = Hours (00h to 23h, BCD format)
CL = Minutes (00h to 59h, BCD format)
DH = Seconds (00h to 59h, BCD format)
DL = Daylight saving flag:
00 = Standard time
01 = Daylight saving time
Returns: Nothing
## 3.3 SAMPLE PROGRAM: DOS DEVICE DRIVER FOR THE AT CLOCK
The following program implements an installable DOS device driver for the AT
clock, using the BIOS RTC functions. Save the following code section as
ATRTC.ASM and assemble according to the instructions in the comment block.
-------------------------------- snip snip snip --------------------------------
NAME ATRTC
; Sample program #1
; DOS Device Driver for the AT Real Time Clock
; Part of the PC Timing FAQ / Application notes
; By K. Heidenstrom (kheidens@actrix.gen.nz)
;
; This program assembles into ATRTC.SYS, an installable DOS device driver that
; removes DOS's dependence on the BIOS timer tick count variable, using the AT
; BIOS's Real Time Clock functions to get and set the current date and time.
; This program does not support the daylight saving feature of the RTC.
; At installation, it checks that the machine is an AT, and that the RTC is
; functional. If either check fails, it installs but remains inactive.
;
; Save this file to ATRTC.ASM and assemble with:
; masm atrtc;
; link atrtc;
; exe2bin atrtc.exe atrtc.sys
; or
; tasm atrtc;
; tlink atrtc;
; exe2bin atrtc.exe atrtc.sys
;
; Then place ATRTC.SYS in your root directory, DOS directory, or utilities
; directory, and add the line DEVICE=<path>\ATRTC.SYS to your CONFIG.SYS
; file, where <path> specifies the directory path to ATRTC.SYS. If you want
; to load ATRTC.SYS high, use DEVICEHIGH= or HIDEVICE= instead of DEVICE= to
; load the driver.
BinFile SEGMENT
ASSUME cs:BinFile,ds:nothing,es:nothing,ss:nothing
ORG 0
Origin:
; Device driver header
Header DD -1 ; Link to next device
Attrib DW 8008h ; Attribute word
DW Strategy ; Strategy entry point
DW Interrupt ; Interrupt entry point
DB "CLOCK$ " ; Device name
; When a request is made for this device, DOS calls the "Strategy" routine,
; passing a pointer to the request header in ES:BX. The strategy routine saves
; this pointer in ReqHdr and returns to DOS. DOS then calls the "Interrupt"
; routine, which executes the request specified by the request header.
ReqHdr DD 0 ; Far pointer to request header
InitPtr DW Init ; Address of init function
MonthTbl1 DW 0,31,59,90,120,151,181,212,243,273,304,334,365 ; Normal
MonthTbl2 DW 0,31,60,91,121,152,182,213,244,274,305,335,366 ; Leap yr
Strategy PROC far ; Save address of Request Header
mov WORD PTR ReqHdr+0,bx
mov WORD PTR ReqHdr+2,es
retf ; Back to DOS
Strategy ENDP
Interrupt PROC far
push ds
push si
push dx
push cx
push bx
push ax ; Preserve registers
lds bx,ReqHdr ; Point DS:BX to Request Header
mov WORD PTR ds:[bx+3],100h ; No errors, completed
mov al,ds:[bx+2] ; Get command number from Request Header
mov cx,OFFSET Read ; Prepare for Read command
cmp al,4 ; Check for Read command
je GotAdr ; If so
mov cx,OFFSET Write ; Prepare for Write command
cmp al,8 ; Check for Write command
je GotAdr ; If so
cmp al,9 ; Check for Write with Verify
je GotAdr ; If so
mov cx,InitPtr ; Prepare for Init command
cmp al,0 ; Check for init command
je GotAdr ; If so
mov cx,OFFSET Null ; If none of above, use Null routine
GotAdr: call cx ; Dispatch to appropriate handler
pop ax
pop bx
pop cx
pop dx
pop si
pop ds ; Restore all regs
retf
Interrupt ENDP
; These command code subroutines called by "Interrupt" Routine. They are called
; with DS:BX pointing to the request header. They do not return an error code.
Read PROC near ; Function 4 = Read
lds bx,ds:[bx+14] ; Point DS:BX to buffer area
push bx ; Keep offset
; Get date, check clock is working
mov ah,4
int 1Ah ; Read RTC date
jnc NoRTCErr1 ; If alright, continue
xor cx,cx ; Assume 1980
jmp SHORT StoreYear ; Don't do calculations
; Calculate year (1980 - 2099) in binary form
; Note - the above check for a date less than 1980 was suggested by Michael
; Mauch (mauch@uni-duisburg.de). He reports that his BIOS (AMI, 06/06/92)
; has a bug which causes years 20xx to be reported as 19xx. The following
; workaround handles this bug.
NoRTCErr1: cmp cx,1980h ; Check for BIOS returning year
jae YearValid ; 19xx when it should be 20xx
mov ch,20h ; If so, fix it
YearValid: mov al,cl ; Get years (00-99)
call BCDToBinary ; Convert to binary
cbw ; Zero AH
push ax ; Keep it
mov al,ch ; Get hundreds of years
call BCDToBinary ; Convert to binary
mov ah,100 ; Factor
mul ah ; Get centuries x 100
pop cx ; Restore year 0-99
add ax,cx ; Now have absolute year in AX.
xor cx,cx ; Zero day counter
mov bx,1980 ; Starting year
; Year calculation stuff - AX is current year (1980 to 2099) read from RTC,
; BX is year being evaluated, CX is count of days so far. SI points to the
; appropriate month table for this year.
; Leap year algorithm: If the year is a multiple of four, it is a leap year,
; unless it's also a century, in which case it is not a leap year, except
; centuries that are a multiple of 400 years (e.g. 2000), in which case it
; is a leap year. In this case, the only century involved is 2000, thus just
; checking for a multiple of four is enough. If it's a multiple of four, it
; is a leap year, i.e. 366 days instead of 365.
;
; Note - There is a way to do this without looping and accumulating, using a
; clever little formula, but I will use this method, because I don't want to
; waste the time I spent getting this method to work :-)
FindYearLp: mov si,OFFSET MonthTbl1 ; Prepare for not leap year
test bl,3 ; Leap year?
jnz NotLeap1 ; If not
mov si,OFFSET MonthTbl2 ; If leap year, use leap year table
NotLeap1: cmp bx,ax ; Got to this year yet?
jae GotYear1 ; If so
add cx,cs:[si+24] ; Add number of days in this year
inc bx ; Increment year number
jmp SHORT FindYearLp ; Loop to find year
; Now have BX containing number of days since 1st of January 1980 for the start
; of the current year - now incorporate the month and the day-of-month.
GotYear1: mov al,dh ; Get month, 1-12, BCD
call BCDToBinary ; Convert to binary
cbw ; Zero AH
shl ax,1 ; Double for word sized table
mov bx,ax ; Month (1-12) to BX
add cx,cs:[si+bx-2] ; Get month start, adjusted for 1-12
mov al,dl ; Get day of month in BCD, 1-31
call BCDToBinary ; Convert to binary
dec ax ; Convert to zero-up
cbw ; Zero hibyte
add cx,ax ; Add in too.
StoreYear: pop bx ; Restore offset of data structure
mov ds:[bx+0],cx ; Store days since 1980 in structure
mov ah,2
int 1Ah ; Read RTC time
jnc NoRTCErr2 ; If alright
xor cx,cx ; If bad, zero values
xor dx,dx
NoRTCErr2: mov al,ch ; Hours
call BCDToBinary ; To binary
mov ds:[bx+3],al ; Store in DOS's data structure
mov al,cl ; Minutes
call BCDToBinary ; To binary
mov ds:[bx+2],al ; Store
mov al,dh ; Seconds
call BCDToBinary ; To binary
mov ds:[bx+5],al ; Store seconds
mov BYTE PTR ds:[bx+4],0 ; Hundredths of seconds are zero
Null: ret ; Return to handler dispatcher
Read ENDP
BCDToBinary PROC near ; Convert AL BCD to binary
push cx
mov ch,al ; Copy value to CH
mov cl,4
shr al,cl ; Shift top nibble down
mov cl,10
mul cl ; Get ten times the high digit
and ch,0Fh ; Low digit only in CH
add al,ch ; Add low digit
pop cx
ret ; Destroys AX and flags only
BCDToBinary ENDP
Write PROC near ; Functions 8 and 9 = Write
lds bx,ds:[bx+14] ; Point DS:BX to buffer area
push bx ; Keep for later
mov dx,ds:[bx+0] ; Get number of days since 1980
; Determine the year, by successively accumulating days starting at 1980 until
; we exceed the number of days since 1980 that was provided by DOS. Once we
; pass the right year, adjust the number of days back again. We then have the
; year and the number of days within that year.
mov ax,1980 ; Start at year 1980
xor cx,cx ; Clear day accumulator
DayAddLp2: mov bx,365 ; Assume for 365 days this year
test al,3 ; Is current year a leap year?
jnz NotLeap2 ; If not, keep the 365
inc bx ; If so, use 366
NotLeap2: add cx,bx ; Add number of days in this year
cmp cx,dx ; Have we gone past the year we want?
ja GotYear2 ; If so, have current year in BX
inc ax ; If not, increment the year
jmp SHORT DayAddLp2 ; Loop
GotYear2: sub cx,bx ; Get number of days up to start of year
sub dx,cx ; Get remainder (Months and Days)
mov si,OFFSET MonthTbl1 ; Prepare for not leap year
test al,3 ; Leap year?
jnz NotLeap3 ; If not
mov si,OFFSET MonthTbl2 ; If leap year, use leap year table
; Here, AX contains the absolute year in binary, DX contains the number of
; days offset into that year, in the range 0 - 364 (or 0 - 365 for leap years)
; and SI points to the appropriate month table for the year being set.
NotLeap3: mov bl,100 ; Divisor
div bl ; Get AL = century (19 or 20), AH = year
mov bx,ax
call BinaryToBCD ; Convert century to BCD
xchg al,bh ; To BH, get year within century
call BinaryToBCD ; To BCD
xchg al,bl ; To BL, and get year in binary to AL
push bx ; Keep value for CX for Set RTC Date
; Now calculate month and day of month from number of days offset into year (DX)
xor bx,bx ; Point to start of table
CompareMonth: inc bx
inc bx ; Move to next month entry
cmp dx,cs:[si+bx] ; Compare to start of next month
jae CompareMonth ; If DX is not less than table entry
sub dx,cs:[si+bx-2] ; Subtract number of days in months
; Now have DL = day of month (zero-up), and BL = month of year (1-12) x 2.
xchg ax,dx ; Get day of month (0-30) to AL
inc ax ; Convert to 1-31
call BinaryToBCD ; Convert to BCD
xchg ax,dx ; To DL
xchg ax,bx ; Get month x 2 from BL
shr al,1 ; Get month number, 1-12
call BinaryToBCD ; Convert to BCD
mov dh,al ; To DH
pop cx ; Restore years and hundreds of years
mov ah,5
int 1Ah ; Set RTC date
; Now set the time
pop bx ; Restore pointer to DOS's data buffer
mov al,ds:[bx+5] ; Read seconds from DOS
call BinaryToBCD ; Convert to BCD
mov dh,al ; To DH
xor dl,dl ; No daylight saving flag
mov al,ds:[bx+3] ; Read hours
call BinaryToBCD ; Convert to BCD
mov ch,al ; To CH
mov al,ds:[bx+2] ; Read minutes
call BinaryToBCD ; Convert to BCD
mov cl,al ; To CL
mov ah,3
int 1Ah ; Set RTC time
ret ; Return to handler dispatcher
Write ENDP
BinaryToBCD PROC near ; Convert AL binary to BCD
xor ah,ah ; Zero hibyte
mov cl,10
div cl ; Div 10 - quotient AL, remainder AH
mov cl,4
shl al,cl ; Shift quotient to top nibble
or al,ah ; Combine two nibbles into AL
ret ; Destroys AX, CL and flags
BinaryToBCD ENDP
Discard: ; End of resident portion of driver
SignOnMsg DB 13,10,"ATRTC - DOS Device Driver for the AT Real Time Clock"
DB 13,10,9,"Part of the PC Timing FAQ / Application notes"
DB 13,10,9,"By K. Heidenstrom (kheidens@actrix.gen.nz)"
DB 13,10,"$"
InstalledMsg DB 9,"Installed",13,10,"$"
NoClockMsg DB 9,"Error - RTC not active",13,10,7,"$"
Init PROC near ; Function 0 = Initialise Driver
mov WORD PTR ds:[bx+14],OFFSET Discard ; Tell DOS where
mov ds:[bx+16],cs ; free memory starts
mov ax,0F000h ; BIOS code segment
mov ds,ax
cmp BYTE PTR ds:[0FFFEh],0FDh ; Check for AT
pushf ; Preserve result
push cs
pop ds ; Point DS to our segment address
ASSUME ds:BinFile
mov WORD PTR InitPtr,OFFSET Null ; Point INIT at Null proc
mov dx,OFFSET SignOnMsg
mov ah,9
int 21h ; Display signon message
popf ; Are we running on an AT?
jae RTCError ; If not, error!
mov ah,4
int 1Ah ; Read date
mov dx,OFFSET InstalledMsg ; Point to 'installed' message
jnc NoRTCError ; If RTC is working, skip error stuff
RTCError: mov BYTE PTR Attrib,0 ; Error - clear CLOCK attribute bit
mov dx,OFFSET NoClockMsg
NoRTCError: mov ah,9
int 21h ; Display error or installation message
ret
Init ENDP
BinFile ENDS
END Origin
-------------------------------- snip snip snip --------------------------------
{TOR} points out that using this driver will result in increased overhead,
because: "the CLOCK$ device is read VERY often by DOS. I did look at this
once, and _as_far_as_I_remember_, CLOCK$ is read on every file access".
Though I don't believe this is a problem, the efficiency of this driver in
cases where frequent file accesses are made could be improved by caching the
date and time values and the BIOS tick count variable each time the date and
time are requested, and only re-reading the RTC if the tick count has changed.
You would use the following logic when the date and time are requested:
Read the current BIOS tick count variable and compare to the stored value.
If same, copy the cached date and time values into the data area and return.
If different, copy the current BIOS tick count variable to the stored value,
read the RTC and recalculate the date and time values, store the new values to
the variables and copy them to the data area and return.
This method would ensure that the RTC is actually accessed no more often than
18.2065 times per second. If frequent file accesses are made, the overhead of
reading the RTC is avoided for most of them.
Michael Bishop (mxbish2@lookout.ecte.uswc.uswest.com) reports that DOS loses
time noticeably on his machine which is: "an IBM PS/Note laptop 25MHz 386,
essentially a PS/2 Model 70/80". While the machine is running, time runs slow.
After a reboot, the time is restored correctly. This symptom indicates that
the machine is missing timer ticks (see sections »» 4.1, »» 6.1, and »» 10.15
for details). Michael was unable to find the IBM driver 'CMOSCLK.SYS' to fix
this, but reports that ATRTC fixed the problem.
## 3.4 OTHER BIOS TIME AND ALARM FUNCTIONS
The RTC can generate an alarm at a specific time of day (i.e. every 24 hours)
until disabled by software. The hardware is more flexible than this (see
section »» 7.35) but the BIOS function only supports one alarm per day.
The alarm is signalled via int 4A hex, which is invoked by the BIOS when the
alarm triggers. Normally int 4Ah points to an IRET. Int 4Ah is invoked under
interrupt, so the normal considerations for hardware interrupt handlers apply
(see section »» 6.23 through »» 6.26).
Int 4Ah will normally be called with interrupts disabled, but don't count on
it. Disable interrupts explicitly if required. The int 4Ah handler must not
destroy any working registers.
The related BIOS functions are as follows. Note that these functions are only
supported on the AT and later machines - the PC and XT do not support them.
Set 24-Hour Alarm Time of Day : int 1Ah
Call with: AH = 06 hex
CH = Hours (00h to 23h, BCD format)
CL = Minutes (00h to 59h, BCD format)
DH = Seconds (00h to 59h, BCD format)
Returns: Nothing
Note: When alarm occurs, int 4Ah is invoked
Disable 24-Hour Alarm : int 1Ah
Call with: AH = 07 hex
Returns: Nothing
Functions 8, 9, 0Ah, and 0Bh are supported on some IBM models.
See Ralf Brown's Interrupt List (see section »» 12) for more information.
## 3.5 OTHER OTHER BIOS TIME FUNCTIONS
The BIOS on the AT and later provides int 15h functions 83h and 86h which use
the RTC interrupt (1024 interrupts per second on IRQ8, int 70h). See section
»» 7.35 for more information about the RTC chip, section »» 7.36 for details of
the RTC interrupt and how to use it, and section »» 7.36.1 for information on
these BIOS functions.
## 3.6 THE TIMES THEY ARE A-CHANGIN'
Any technique that makes use of a time taken from the RTC or derived from the
tick count should take into account the fact that the time can be changed by
the user, or even by other software. This can cause the time to go forwards
or backwards slightly, or even jump to a totally different time.
Under real DOS, normally this will only happen to a TSR or a program that shells
to DOS, where the user may change the time via the TIME command, or a program
that allows the user to change the time. A networked computer may automatically
update its time from the server, via the resident network software.
On a machine running a multitasking operating system such as OS/2, Linux, Win95,
and even Windoze, changing the system date and time in one session will change
the time in all sessions.
## 4 USING THE BIOS TICK COUNT VARIABLE
The BIOS tick count variable gives an absolute time reference with a resolution
of 54.9254 milliseconds.
## 4.1 THE BIOS TICK COUNT VARIABLE
The BIOS tick count variable is a 32-bit unsigned longword or DWORD, stored at
low memory address 0040:006C (can also be addressed as 0000:046C), maintained
by the BIOS's int 8 handler. It contains the number of timer ticks (units of
54.9254 ms) since midnight, in the current day. The maximum value in this
variable is 1800AF hex, so only the bottom 21 bits can ever be nonzero.
The PC and XT have no special real-time clock support in the BIOS, so the tick
counter is initialised to zero on every reboot. In ATs and later machines, the
BIOS's power-on initialisation code reads the real-time clock and sets the tick
count variable to the equivalent number of ticks. See section »» 10.15.
There are approximately 65536 ticks in an hour (65543.4265 to be exact), so the
high word of the tick count corresponds _approximately_ to the hour of the day.
## 4.2 CHANGE OF DAY
There are 1,573,042.24 ticks in a day, but the BIOS writers approximated the
CTC clock to 1.193180 MHz, so the BIOS uses 1,573,040 (001800B0 hex) ticks per
day. This gives a 1.42166 ppm error (0.123 seconds per day), which is fairly
insignificant compared to the clock frequency inaccuracy (see section »» 7.2).
The tick count increments up to 001800AF hex, then 'rolls over' to zero at
midnight. When midnight passes, the BIOS sets the one-byte 'midnight' flag at
0040:0070, to 1, indicating that a midnight has passed. Note - some BIOSes may
indicate change of day by _incrementing_ the midnight flag byte, so that if two
midnights pass without DOS reading the time, the date could still be updated
correctly. See section »» 10.15 for details.
## 4.3 READING AND SETTING THE TICK COUNT
You can read the tick count directly, or request it from the BIOS via int 1Ah.
Get Tick Count : int 1Ah
Call with: AH = 00 hex
Returns: CX = High word of tick count
DX = Low word of tick count
AL = Midnight-passed flag
Notes: This call clears the midnight flag byte.
Notes: Do not use this call in an application - see below
Set Tick Count : int 1Ah
Call with: AH = 01 hex
CX = High word of tick count
DX = Low word of tick count
Notes: This call clears the midnight flag byte.
The DOS CLOCK$ device driver uses the Get Tick Count function, int 1Ah, function
0, and relies on the midnight flag returned by this function to detect a change
of day. User programs should not use these two BIOS functions, because if the
program calls the function just after midnight, it will see the midnight flag,
and the midnight flag will be cleared, so DOS will miss out on seeing the change
of day, and will not increment the date. See sections »» 10.15 and »» 10.16.
This problem would be solved if DOS used the real-time clock for timekeeping
(see section »» 3.3 for a DOS device driver that uses the real-time clock).
It is safer and more efficient to read (and write) the count directly at its
location in low memory. The tick count is 'volatile', and must be accessed
with an indivisible operation (using a 32-bit register such as EAX), or with
interrupts disabled. If you access the loword and hiword separately without
disabling interrupts around the two accesses, a tick interrupt could come
along and modify the tick count variable between the two reads or writes.
See section »» 4.5 for details.
## 4.4 SPECIAL REQUIREMENTS - NONE
The great advantage of timing using the BIOS tick count, is that it makes no
changes to the system, i.e. it doesn't change the hardware setup, or modify any
interrupt vectors. This simplifies the code, and means that if the program is
terminated (by Ctrl-Break, or a Divide Overflow, or by the user replying 'A' to
the Abort, Retry, Ignore prompt), no special clean-up is required.
## 4.5 SAMPLE PROGRAM: READING THE TICK COUNT
The function read_bios_tick_count() reads and returns the BIOS tick count. The
function has_tick_occurred() detects whether the tick count has changed since
the last time that function was called. It returns TRUE on the initial call.
It does not report _how_many_ timer ticks occurred between calls.
Notice that read_bios_tick_count() explicitly disables interrupts around the
read of the 32-bit tick count value. Even though the tick count variable is
declared as volatile, the compiler (Borland C++ 2.0) generates two 16-bit MOV
instructions without disabling interrupts. If an interrupt occurred between
the two MOV instructions, an incorrect value will be read. Apparently this is
not a bug, it is because the compiler doesn't know how to safely read 'volatile'
variables. Hmm. I'd say if it's not a bug, it's definitely a mis-feature.
If the compiler can use the 32-bit registers (compiling for protected mode, or
compiling with 32-bit code under DOS, this problem does not (or should not!)
occur. Michael Mauch (mauch@uni-duisburg.de) found that Borland C++ 4.0 does
use a 32-bit MOV instruction if 32-bit code generation is enabled via #pragma
option -3 or #pragma option -4.
Dr. John Stockton (see section »» 1.7) reports that this problem also exists in
Borland Pascal 7 when a signed long variable (BP7 doesn't have _unsigned_ longs)
is loaded from the tick count variable, as the tick count is read non-atomically
with two 16-bit accesses. Disabling interrupts around the load prevents the
problem described above.
See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-------------------------------- snip snip snip --------------------------------
/*
Sample program #2
Demonstrates reading the BIOS tick count
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save this file to SAMPLE2.C and compile with:
bcc -I<inc_path> -L<lib_path> -ms sample2.c
Where inc_path is the path to your C header files and your startup modules
C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, and cli */
#include <stdio.h> /* Pass go, add printf(), program is 8K already :-) */
#include <stdlib.h> /* Needed for exit() */
#define FALSE 0
#define TRUE 1
#define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL)
unsigned long read_bios_tick_count(void) {
unsigned long ct;
asm pushf; /* Preserve interrupt flag */
asm cli; /* Needed even though tick count is volatile */
ct = * BIOS_TICK_COUNT_P;
asm popf; /* Restore interrupt flag */
return ct;
}
int has_tick_occurred(void) {
static unsigned long old_tick_count = 0xFFFFFFFFL; /* Invalid */
if (read_bios_tick_count() != old_tick_count) { /* Changed? */
old_tick_count = read_bios_tick_count();
return TRUE;
}
return FALSE; /* No change */
}
void main(void) {
unsigned int n = 0;
printf("Sample program #2 - Demonstrates reading the BIOS tick count variable\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
while (n < 18) /* Stop after one second */
if (has_tick_occurred())
printf("Tick %d: BIOS tick count variable = %ld\n",
++n, read_bios_tick_count());
exit(0);
}
-------------------------------- snip snip snip --------------------------------
## 4.6 SAMPLE CODE: OPTIMISED FUNCTION TO READ THE TICK COUNT
This is a more optimal coding of read_bios_tick_count() in assembler. I chose
to disable interrupts and read the loword and hiword separately, rather than
using LES or LDS (indivisible operations) because it is not good practice to
load a segment register with a value which is not a real segment-paragraph.
Of course if your code requires a 386 or higher, you can just load an extended
(32-bit) register (e.g. EAX) in one single indivisible operation.
-------------------------------- snip snip snip --------------------------------
; Function to read the BIOS tick count (C-callable)
; Part of the PC Timing FAQ / Application notes
; By K. Heidenstrom (kheidens@actrix.gen.nz)
;
_read_bios_tick_count PROC near ; or FAR for far code model
; unsigned long read_bios_tick_count(void);
push ds ; Preserve data segment
pushf ; Keep interrupt flag
xor ax,ax ; Zero
mov ds,ax ; Address BIOS data area
cli ; Don't want a tick to interrupt us
mov ax,ds:[46Ch] ; Get loword of count
mov dx,ds:[46Eh] ; Get hiword of count
popf ; Restore interrupt flag as provided
pop ds ; Restore data segment
ret ; Return tick count in DX|AX
_read_bios_tick_count ENDP
-------------------------------- snip snip snip --------------------------------
## 4.7 SAMPLE PROGRAM: USING THE TICK COUNT FOR TIMEOUT CHECKING
This example demonstrates two independent timeout counters using the BIOS tick
count variable. The timeout counter record consists of the starting tick count,
the number of ticks in the timeout period, and a flag which can be used to
report the transition to the timed-out state.
set_timeout() sets up a timeout counter. The state of the timeout can then be
requested using is_timedout() and just_timedout(). is_timedout() returns TRUE
if the current time is outside the timeout period specified by the counter.
just_timedout() returns TRUE the first time it is called after the timeout
expires, and from then on, returns FALSE until a new timeout is configured.
The timeout may occur up to one tick earlier than expected, depending on the
synchronisation between setting the timeout, and the actual timer tick. A one
tick timeout will time out on the next tick that occurs after the timeout was
set up, so if the timeout is set just after a tick has occurred, the timeout
will occur nearly 54.9254 ms later, but if the timeout is set just before a
tick, the timeout will occur almost immediately. See section »» 10.10 for
more details.
Because the tick count restarts at midnight, leaving a timeout active for a
whole day will cause the timeout state to change. For example a ten minute
timeout will expire after ten minutes, but every day thereafter, from the time
that the timeout started, the timeout function will report not timed out for
ten minutes.
This demo program uses two timeout counters, and waits for ten keypresses. One
timeout counter is used as a global timeout for the whole program, set to 20
seconds. The other timeout is used as a timeout for each individual keypress.
To avoid both timeouts, you must press any key ten times within a total of 20
seconds, with no more than four seconds elapsing between the keys. So, it's a
demo! I didn't say it would be useful. Call it a game of skill :-)
-------------------------------- snip snip snip --------------------------------
/*
Sample program #3
Demonstrates multiple timeouts using the BIOS tick count
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save this file to SAMPLE3.C and compile with:
bcc -I<inc_path> -L<lib_path> -ms sample3.c
Where inc_path is the path to your C header files and your startup modules
C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, and cli */
#include <stdio.h> /* Needed for printf() */
#include <stdlib.h> /* Needed for exit() */
#define NTIMEOUTS 2 /* Set this to however many timeouts you need */
#define GLOBAL_TIMEOUT 0 /* Counter number to use for global timeout */
#define CHAR_TIMEOUT 1 /* Counter to use for per-character timeout */
#define FALSE 0
#define TRUE 1
unsigned long timeoutstart[NTIMEOUTS]; /* Starting tick value per timeout */
unsigned int timeoutlength[NTIMEOUTS]; /* Timeout period (ticks) per timeout */
unsigned int timeoutflag[NTIMEOUTS]; /* Flags for timeout state */
#define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL)
#define TICK_WRAP 0x001800B0L /* Past last value of tick count */
unsigned long read_bios_tick_count(void) {
unsigned long ct;
asm pushf;
asm cli;
ct = * BIOS_TICK_COUNT_P;
asm popf;
return ct;
}
/* tick_diff(), returns the difference between two timer tick counts. This
is just new value minus old value, except if the period crosses midnight. */
unsigned long tick_diff(unsigned long start_tick, unsigned long now_tick) {
signed long diff;
if (start_tick >= TICK_WRAP || now_tick >= TICK_WRAP)
return 0xFFFFFFFFL; /* Invalid */
diff = now_tick - start_tick;
if (diff < 0)
diff += TICK_WRAP;
return (unsigned long) diff;
}
/* Set a timeout counter for timeout after a specific number of ticks */
void set_timeout(unsigned int timeoutnum, unsigned int timeoutticks) {
if (timeoutnum >= NTIMEOUTS)
return;
timeoutstart[timeoutnum] = read_bios_tick_count(); /* Start time */
timeoutlength[timeoutnum] = timeoutticks; /* Duration */
timeoutflag[timeoutnum] = FALSE;
return;
}
/* Returns whether the nominated counter is in the timed-out state. After the
timeout has expired, this function will return TRUE, until a new timeout
period is set. Do not leave timeouts active for periods approaching one
day, as this will cause the timeout state to be incorrectly reported as
FALSE for the same period of each day. */
int has_timedout(unsigned int timeoutnum) {
if (timeoutflag[timeoutnum])
return TRUE; /* Latch the timed-out state */
return (tick_diff(timeoutstart[timeoutnum], read_bios_tick_count()) >= timeoutlength[timeoutnum]);
}
/* Test whether a counter has just timed out. Returns TRUE only the
first time it is called after the timeout occurs. */
int just_timedout(unsigned int timeoutnum) {
if (timeoutflag[timeoutnum] == TRUE) /* Already reported timeout */
return FALSE;
if (has_timedout(timeoutnum)) { /* Timeout has expired */
timeoutflag[timeoutnum] = TRUE;
return TRUE;
}
return FALSE; /* Timeout has not expired yet */
}
void main(void) {
unsigned int n, key;
printf("Sample program #3 - Demonstrates multiple timeouts using the BIOS tick count\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
printf("Press any key ten times\n");
printf("The timeout on each character is four seconds\n");
printf("The overall timeout on all ten characters is twenty seconds\n\n");
set_timeout(GLOBAL_TIMEOUT, 364); /* Global timeout 20 sec */
for (n = 0; n < 10; ++n) { /* Read ten characters */
set_timeout(CHAR_TIMEOUT, 73); /* Char timeout 4 sec */
while (TRUE) {
if (just_timedout(CHAR_TIMEOUT)) {
printf("Timed out on single character\n");
exit(1);
}
if (just_timedout(GLOBAL_TIMEOUT)) {
printf("Global timeout expired\n");
exit(2);
}
if (bioskey(1)) {
key = bioskey(0);
break;
}
}
printf("Key pressed: %c\n", key);
}
printf("Neither timeout expired; normal program termination");
exit(0);
}
-------------------------------- snip snip snip --------------------------------
## 4.8 SIMPLE DELAYS USING THE BIOS TICK COUNT
A simple way to implement delays of about 0.1 seconds or longer with one tick
resolution, or perform timeout checking, is to provide a function that waits
for a tick to occur, such as the following function:
-------------------------------- snip snip snip --------------------------------
void wait_next_tick(void) {
static unsigned int last_tick_loword;
unsigned int now_tick_loword;
do {
now_tick_loword = * ((volatile unsigned int far *) 0x0040006CL);
} while (now_tick_loword == last_tick_loword);
last_tick_loword = now_tick_loword;
return;
}
-------------------------------- snip snip snip --------------------------------
This function can then be called in a loop, e.g.
for (n = 0; n < 10; ++n)
wait_next_tick();
to implement a delay of the desired number of ticks. A modified method can
be used to implement timeout checking with regular polling of some input
device such as a serial port buffer, or the keyboard.
There is no need to use the hiword of the BIOS tick count variable; just
checking for a change in the loword is enough to detect that a tick has
occurred.
## 5 SPECIAL SOFTWARE PRECAUTIONS
If your program intercepts any interrupt vectors (e.g. int 8 or int 1Ch), or
reprograms the RTC or other hardware into a strange mode, it must restore
hardware states and interrupt vectors (i.e. clean up) if terminated by DOS,
or risk having its interrupt handlers overwritten by another program and
causing a system crash or causing incorrect operation due to the hardware
being in the wrong state.
You should handle the following interrupts:
■ DOS Ctrl-C interrupt
■ DOS Critical Error interrupts
■ Divide Overflow interrupt (optional)
Here are the gory details. For more details try DOS technical books such as
the MS-DOS Encyclopedia or Ralf Brown's venerated Interrupt List (see section
»» 12 for details of both of these references).
## 5.1 THE CTRL-C AND CTRL-BREAK INTERRUPTS
Int 23h is the DOS Ctrl-C interrupt. It is invoked by DOS whenever a Ctrl-C
character (ASCII code 3) is detected in the keyboard input stream. When the
Ctrl-Break combination is pressed, the BIOS issues int 1Bh (the Ctrl-Break
interrupt), and DOS's int 1Bh handler sets an internal flag in DOS that causes
a faked Ctrl-C to appear in the input.
Thus, by trapping Ctrl-C, you are trapping Ctrl-Break too, except that Ctrl-C
will only be registered when DOS input is read, while Ctrl-Break is generated
as soon as the keystroke is accepted. Also, if input redirection is used, the
Ctrl-C interrupt may not be registered properly, depending on the 'BREAK='
setting in CONFIG.SYS.
## 5.2 HANDLING THE CTRL-C INTERRUPT
You can just replace the default int 23h handler using setvect() (DOS function
25h), there's no need to save the previous vector contents because DOS will
restore the vector for you when the program exits or is terminated. However,
if you intercept int 1Bh as well as int 23h, DOS will not restore int 1Bh
when your program terminates, so your program will have to do this itself.
Typical actions for a Ctrl-C interrupt handler include:
■ Do nothing and rely on the Ctrl-C appearing in the keyboard input
stream to the program (this will only happen if the program reads
its keyboard input via DOS, not via the BIOS),
■ Set a flag which will be checked by the program's mainline and will
cause the mainline to take some appropriate action (e.g. clean up
and terminate the program),
■ Call a general 'user interruption' function inside the main portion
of the program, which registers the Ctrl-C request, and/or takes an
appropriate action,
■ Restore interrupt vectors, restore normal hardware states, clean up,
and terminate the program immediately, by itself.
All DOS functions can be called from within a Ctrl-C interrupt handler. Some C
library functions may not be safe to call - for instance, the function which was
reading the DOS keyboard input when the Ctrl-C was detected, will be in progress
and might not be re-entrant - see your compiler's library reference for details.
On entry to the Ctrl-C interrupt handler, interrupts will be disabled, and it
would normally be appropriate to enable them, using enable() or STI, unless the
handler will always return quickly.
If the handler returns control to DOS, an IRET instruction should be used.
There is no return value. General registers may be modified by the Ctrl-C
handler. Alternatively, the handler may call DOS to terminate the program
(e.g. via DOS function 4Ch, terminate with return code).
## 5.3 THE CRITICAL ERROR INTERRUPT
Int 24h is the DOS Critical Error interrupt, and is issued by DOS when a device
driver indicates a critical failure.
The default critical error handler issues the familiar "Abort, Retry, Ignore?"
prompt. You can replace the default handler using setvect() (DOS function 25h).
DOS will restore the vector for you when the program exits or is terminated.
On entry to the int 24h handler, registers AX, SI, DI, BP contain information
about the nature of the critical error, and the stack contains the values in all
registers as provided to the int 21h call which caused the critical error to
occur, as well as the return address for the int 21h call. For these reasons,
int 24h handlers are usually written in assembler.
## 5.4 CRITICAL ERROR HANDLER PARAMETERS
On entry to the int 24h handler, the stack is arranged thus:
[SS:SP+0] IP (PC) of return address for int 24h handler
[SS:SP+2] CS of return address for int 24h handler
[SS:SP+4] Flags for return of int 24h handler
[SS:SP+6] AX as provided to int 21h invocation
[SS:SP+8] BX as provided to int 21h invocation
[SS:SP+0Ah] CX as provided to int 21h invocation
[SS:SP+0Ch] DX as provided to int 21h invocation
[SS:SP+0Eh] SI as provided to int 21h invocation
[SS:SP+10h] DI as provided to int 21h invocation
[SS:SP+12h] BP as provided to int 21h invocation
[SS:SP+14h] DS as provided to int 21h invocation
[SS:SP+16h] ES as provided to int 21h invocation
[SS:SP+18h] IP (PC) of return address for int 21h invocation
[SS:SP+1Ah] CS of return address for int 21h invocation
[SS:SP+1Ch] Flags for return from int 21h invocation
On entry to the int 24h handler, BP:SI contain the segment:offset address of
the device driver header of the device which flagged the critical error.
The high eight bits of the DI register are undefined. The lower eight
bits of DI contain the error description, as follows:
0 = Write-protected disk, 1 = Unknown unit, 2 = Drive not
ready, 3 = Invalid command, 4 = Data error, 5 = Invalid request
structure length, 6 = Seek error, 7 = Non-DOS disk, 8 = Sector
not found, 9 = Out of paper (printer), 10 = Write fault,
11 = Read fault, 12 = General failure, 15 = Invalid disk
change (DOS 3.0 and later).
Many of these error codes are not applicable to character devices.
If bit 7 of AH is set, the critical error occurred on a character device (e.g.
PRN or AUX), and all other bits of AX are undefined.
If bit 7 of AH is clear, the critical error occurred on a block device (i.e. a
disk drive), and the error location is described by the remaining bits in AX.
AL contains the drive designator minus 41 hex (i.e. 0 means drive A, 1 means
drive B, 2 means drive C, etc). AH describes the error location, as follows:
7 6 5 4 3 2 1 0
* . . . . . . . 0 (Error occurred on block device)
. * . . . . . . Not used
. . * . . . . . "Ignore" allowed? (0 = no, 1 = yes) (3.1+)
. . . * . . . . "Retry" allowed? (0 = no, 1 = yes) (3.1+)
. . . . * . . . "Fail" allowed? (0 = no, 1 = yes) (3.1+)
. . . . . * * . Location: 00=DOS, 01=FAT, 10=Root, 11=Files
. . . . . . . * Read or Write operation (0 = read, 1 = write)
Bits 3, 4, and 5 are only meaningful if the DOS version is 3.1 or later.
The DOS version may be checked from inside the critical error handler, using
DOS function 30 hex, or it may be determined by startup code and stored in a
global variable accessible by the critical error handler.
## 5.5 CRITICAL ERROR HANDLER OPERATION
The critical error handler may use DOS functions 01 through 0Ch (the old CP/M
character I/O functions). It may also use DOS functions 30h and 59h (request
DOS version, and request extended error information). Other DOS functions may
NOT be called, as DOS is mostly non-reentrant.
The critical error handler must preserve all register values, except the flags
(presumably) and AL, which is used to specify the action for DOS to take upon
return from the handler, as follows:
0 = Ignore
1 = Retry
2 = Abort
3 = Fail current function
Ideally, a critical error handler built in to a program should also deallocate
any other resources that that program might have allocated, such as EMS and/or
XMS memory. Temporary files cannot be safely deleted because the DOS file
functions must not be called. Possibly if the handler is going to abort the
program anyway, it may be safe to call these functions. If anyone has detailed
info, please let me know. (*)
## 5.6 THE DIVIDE OVERFLOW INTERRUPT
The divide overflow interrupt is int 0. It is generated by the processor when
the quotient of a signed or unsigned integer division (IDIV or DIV instruction)
would exceed the size of the register into which it would be placed.
DOS's default divide overflow handler issues the message "Divide overflow" and
terminates the current application, giving the program no chance to restore
interrupt vectors, hardware states, or allocated resources, or close files, etc.
Generally if a divide overflow occurs, the user should reboot their system. As
a result (or perhaps the cause) of this, most programs do not provide their own
divide overflow interrupt handlers.
If you wish to handle divide overflows, I would suggest using direct writes to
the interrupt vector table in low memory to restore all intercepted vectors
(except int 23h and 24h, which will be restored by DOS), restoring the hardware
state directly, and perhaps deallocating any resources such as EMS and/or XMS,
then calling DOS function 4Ch to terminate the program.
It might be possible to write a divide overflow handler which resumes execution
after loading an appropriate value into the result register. This requires
scanning the offending instruction, and a detailed knowledge of the operation
of the various x86 processors, and is left as an exercise for the reader :-)
## 5.7 ERROR HANDLING SYSTEM
The error handling system I have used in the sample programs uses a function
prototyped as follows:
void abort_cleanup(int dos_is_safe);
This function is responsible for performing as much cleanup as possible at
program exit time. The dos_is_safe parameter specifies whether DOS functions
may be safely used by the cleanup function. This parameter will be FALSE if the
function is called from within the critical error handler or a divide overflow
handler, and TRUE if the function is called from the Ctrl-C handler or by the
program itself during cleanup for an orderly exit.
abort_cleanup should not exit to DOS itself. This will be done by the caller.
If dos_is_safe is FALSE, your abort_cleanup() function should not call any DOS
functions. Interrupt vectors should be restored using direct accesses into the
interrupt table in low memory (though this technique is frowned upon).
Depending on the types of cleanups required, DOS may _have_ to be called. In
certain circumstances (e.g. after a divide overflow), abort_cleanup() may crash
the machine in its attempt to clean up properly. On the other hand, if it did
not attempt to clean up properly, the machine might be left in an unstable state
anyway. You will have to weigh up the pros and cons when deciding how much to
try to clean up if dos_is_safe is FALSE. Perhaps you should do the most
critical and/or most likely to succeed cleanups first.
## 5.8 SAMPLE CODE MODULE: CRITICAL ERROR HANDLER MODULE
-------------------------------- snip snip snip --------------------------------
NAME CRIT_ERR
; Rudimentary critical error handler module
; Part of the PC Timing FAQ / Application notes
; By K. Heidenstrom (kheidens@actrix.gen.nz)
;
; This module provides rudimentary critical error (int 24h) handling for DOS
; application programs. It is callable from Borland C. This file is written
; for small model (near code, near data). You can change the FAR_CODE equate
; to hopefully make it compatible with other memory models.
;
; This is a minimal implementation for demonstration purposes.
;
; Upon startup, the application should call crit_err_intercept() to install the
; new critical error handler. No corresponding uninstallation is required.
;
; If the user selects the Abort option to the "Abort, Retry, Ignore" prompt,
; the abort_cleanup() function (which is provided externally) is called, with
; its dos_is_safe parameter set to FALSE. This function performs as much
; cleanup as possible (restoring interrupt vectors, restoring hardware states,
; setting normal text video mode, and deallocating resources such as EMS and
; XMS memory. It would also delete temporary files and close any open files if
; dos_is_safe were TRUE. After abort_cleanup() returns, the critical error
; handler returns the Abort code to DOS, which will then abort the program.
;
; Save this file to CRIT_ERR.ASM and assemble with:
; masm /Mx crit_err;
; or
; tasm /mx crit_err;
; to produce CRIT_ERR.OBJ which can be linked into the user program.
FALSE EQU 0
TRUE EQU 1
FAR_CODE EQU FALSE ; TRUE for far code models
; void crit_err_intercept(void);
PUBLIC _crit_err_intercept
; unsigned int is_at_crit_prompt(void);
PUBLIC _is_at_crit_prompt
; void abort_cleanup(int dos_is_safe);
IF FAR_CODE
EXTRN _abort_cleanup : FAR
ELSE
EXTRN _abort_cleanup : NEAR
ENDIF
_DATA SEGMENT
_DATA ENDS
DGROUP GROUP _DATA
_TEXT SEGMENT PARA PUBLIC 'CODE'
ASSUME cs:_TEXT
; Data - in code segment (naughty naughty)
I24_IP DW 0 ; IP for return from int 24h intercept
I24_CS DW 0 ; CS for same
I24_FL DW 0 ; Flags for same
In_Crit DB 0 ; Flag whether currently at int 24h prompt
IF FAR_CODE
_crit_err_intercept PROC far
ELSE
_crit_err_intercept PROC near
ENDIF
; This function intercepts interrupt 24h, and replaces the DOS default int 24h
; handler with the new handler, crit_err_handler. This function should be
; called ONCE and ONLY ONCE at program startup. No corresponding restore-
; interrupt function is required.
mov ax,3524h ; Request int 24h
int 21h
mov cs:[O24_IP],bx ; Self-modifying code?
mov cs:[O24_CS],es ; Where? I didn't see it :-)
push ds
push cs
pop ds
mov dx,OFFSET _TEXT:crit_err_handler
mov ax,2524h ; Set int 24h
int 21h
pop ds
ret
_crit_err_intercept ENDP
IF FAR_CODE
_is_at_crit_prompt PROC far
ELSE
_is_at_crit_prompt PROC near
ENDIF
; This function returns the status of the In_Crit flag, and should be called
; by the Ctrl-C interrupt handler (if any) to check that the Ctrl-C was not
; pressed while at the Abort, Retry, Ignore prompt. The function returns
; FALSE if not at the prompt, or TRUE if at the prompt. If it returns TRUE,
; the Ctrl-C handler is not safe to call general DOS functions.
mov al,cs:[In_Crit]
xor ah,ah
ret
_is_at_crit_prompt ENDP
crit_err_handler PROC far
; This function handles interrupt 24 hex, the DOS Critical Error interrupt.
; It calls the original DOS interrupt 24h handler, and checks the returned
; action code.
; If the action code is 2 (abort), it calls abort_cleanup() (provided by the
; application), passing a FALSE value for the dos_is_safe parameter. In
; either case, it then returns the user-specified action code to DOS.
;
; See documentation above for details of the abort_cleanup() function.
pop cs:[I24_IP] ; IP of return address of int 24h
pop cs:[I24_CS] ; CS of return address of int 24h
pop cs:[I24_FL] ; Flags of int 24h invocation
mov cs:[In_Crit],1 ; Set flag
pushf ; Simulate an INT
DB 9Ah ; CALL xxxx:xxxx
O24_IP DW 0 ; Offset of call (modified)
O24_CS DW 0FFFFh ; Segment of call (modified)
mov cs:[In_Crit],0 ; Clear flag
cmp al,2 ; Did user choose Abort?
jne NotAbort ; If not
push es ; If so, call abort_cleanup()
push ds
push di
push si
push bp
push dx
push cx
push bx
push ax
mov ax,SEG DGROUP
mov ds,ax ; Set up DS for call to C function
xor ax,ax ; dos_is_safe is FALSE!
push ax
call _abort_cleanup
pop ax ; Discard parameter
; mov ax,0E07h ; Enable these lines during debugging
; xor bx,bx ; to generate a beep after your
; int 10h ; abort_cleanup() function completes
pop ax
mov al,2 ; Restore the Abort code
pop bx
pop cx
pop dx
pop bp
pop si
pop di
pop ds
pop es
NotAbort:
push cs:[I24_FL] ; Flags of int 24h invocation
push cs:[I24_CS] ; CS of return address of int 24h
push cs:[I24_IP] ; IP of return address of int 24h
iret
crit_err_handler ENDP
_TEXT ENDS
END
-------------------------------- snip snip snip --------------------------------
Gian Uberto Lauri (saint@dei.unipd.it) sent me a modified version of this
module with the names changed to support the Borland C++ 3.1 compiler when
compiling a C++ program. Borland C++ encodes the parameter types in the
function name ("mangling"). Gian had to change the names of the functions
as follows:
_crit_err_intercept --> @crit_err_intercept$qv
_is_at_crit_prompt --> @is_at_crit_prompt$qv
_abort_cleanup --> @abort_cleanup$qi
These names must be changed both at the PUBLIC or EXTRN declarations, and at
the actual PROC and ENDP lines. These changes are specific to Borland C++.
Other C++ compilers will handle this differently.
## 6 INTERRUPTS
An interrupt is an interruption to the processor, that causes it to stop what
it is doing and jump to a specially written subroutine, known as an interrupt
handler, interrupt routine, or interrupt service routine (ISR).
There are three types of interrupts - processor-generated interrupts, external
hardware interrupts, and software interrupts. Processor-generated interrupts
are generated internally by the processor (Intel 80x86) in certain conditions,
such as a division overflow (see section »» 5.6). External hardware interrupts
are generated by IRQs, and are described shortly. Software interrupts are
invoked by software, and are generally used for calling system functions, e.g.
BIOS functions, DOS functions, mouse functions, EMS functions, etc.
Interrupts are identified by an interrupt number, in the range 0 to 0FFh.
┌───────────────────────┬───────────────────────────────────────────────┐
│ Interrupt numbers │ Interrupt type │
├───────────────────────┼───────────────────────────────────────────────┤
│ 0,1,2,3,4 │ Processor │
│ 5,6,7 │ Software and processor │
│ 8-0Fh │ Hardware (IRQ0-7) and processor │
│ 10h-6Fh │ Software (some are also processor interrupts) │
│ 70h-77h │ Hardware (IRQ8-15) │
│ 78h-0FFh │ Software │
└───────────────────────┴───────────────────────────────────────────────┘
Some low-numbered interrupts have a split personality, because IBM ignored
Intel's "reserved for processor" comment on the first 32 interrupts. The
original 8086/8088 only used ints 0, 1, 2, 3, and 4 for processor interrupts,
so IBM used ints 5 and upwards for hardware and software interrupts. With
later x86 processors, Intel reclaimed their reserved interrupts, requiring
special support in the EMM386 driver to handle these interrupts properly.
See section »» 6.7 for details.
Tor Sjowall {TOR} points out that these conflicts only occur in real mode and
virtual 86 mode. In protected mode, there is no such conflict - the processor
interrupts have their Intel defined functions, the hardware interrupts are
vectored through different interrupts, and the software interrupts are not
relevant (since they relate to DOS and BIOS, which are not protected mode
programs).
Software interrupts 23h and 24h (Ctrl-C and Critical Error) are described in
section »» 5 and subsections. Software interrupt 1Ch and hardware int 8 are
the timer tick interrupts, and are described in section »» 6.1.
## 6.1 THE TIMER TICK INTERRUPTS
Interrupt 8 and interrupt 1C hex are the timer tick interrupts.
Int 8 is a hardware interrupt, invoked directly by IRQ0, from CTC channel zero,
and is the highest priority IRQ (unless interrupt priorities have been changed
from the BIOS defaults). The BIOS POST sets int 8 to point to the BIOS's int 8
interrupt service routine, traditionally located at F000:FEA5, which performs
the delayed floppy disk motor turn-off and updates the system time-of-day.
Device drivers and TSRs often intercept this interrupt, so often the vector
won't point directly to the BIOS.
Int 1C hex is issued (i.e. generated) by the BIOS's int 8 service routine, and
normally points to an IRET instruction in the BIOS. Int 1Ch is intended to be
used by application programs which require a regular interrupt source.
Some TSRs also hook this interrupt - see section »» 6.35 for details.
## 6.2 INTERRUPT VECTOR TABLE
The interrupt vector table, or IVT, is a reserved area of RAM occupying the
bottom kilobyte of main memory, i.e. from 0000:0000 to 0000:03FF. This is in
real mode or virtual 8086 mode, under DOS. In protected mode this is probably
completely different. (*)
Each interrupt has a corresponding four-byte far code pointer, located at
interrupt number x 4 bytes into the IVT, which points to the interrupt service
routine that will be invoked when that interrupt is registered by the processor.
For example, here is a dump of the first 128 bytes of the IVT on my machine:
0000:0000 1A 00 70 00 05 00 70 00 1B 2C 5D 57 05 00 70 00 ..p...p..,]W..p.
0000:0010 05 00 70 00 54 FF 00 F0 4C E1 00 F0 6F EF 00 F0 ..p.T..pLa.poo.p
0000:0020 57 01 80 E6 AD 2B 5D 57 6F EF 00 F0 45 10 1F CF W..f-+]Woo.pE..O
0000:0030 6F EF 00 F0 6F EF 00 F0 57 EF 00 F0 6F EF 00 F0 oo.poo.pWo.poo.p
0000:0040 C6 01 80 E6 4D F8 00 F0 41 F8 00 F0 C0 05 A2 D1 F..fMx.pAx.p@."Q
0000:0050 39 E7 00 F0 18 00 55 02 20 01 B3 E5 D2 EF 00 F0 9g.p..U. .3eRo.p
0000:0060 D4 E3 00 F0 65 0F A2 D1 6E FE 00 F0 64 06 70 00 Tc.pe."Qn~.pd.p.
0000:0070 1B 91 A1 03 A4 F0 00 F0 22 05 00 00 6E 42 00 C0 ..!.$p.p"...nB.@
The vector for interrupt 1C hex starts at 1Ch x 4, which is 70 hex. In the
above vector table contents, the vector at 0000:0070 points to 03A1:911B, so
every time int 1Ch is issued, the processor will jump to 03A1:911B and execute
the ISR that starts at that address.
## 6.3 INTERCEPTING AN INTERRUPT
To take control of an interrupt, use the getvect() and setvect() functions or
DOS functions 35 hex and 25 hex. If necessary, you can directly access the
interrupt vector table in low memory (see section »» 6.2). This may be required
if DOS cannot safely be called - for example, in a critical error handler (see
section »» 5.3). Interrupts MUST be locked out while any direct manipulation
of this type is performed.
Start by requesting the contents of the interrupt vector, using getvect() or
DOS function 35 hex. This gives a far code pointer, which must be stored to be
reinstated when your program terminates. The stored 'old interrupt' vector is
also used for interrupt chaining (section »» 6.31). Then, set the interrupt
vector to point to your new handler, using setvect() or DOS function 25 hex,
and away you go.
See section »» 5 and subsections for details of intercepting the DOS Ctrl-C and
critical error interrupts and the divide overflow interrupt, which must be done
to ensure that your program reinstates the original interrupt owner upon exit.
## 6.4 INTERRUPT HARDWARE
Hardware interrupts are known as IRQs (interrupt requests). They interrupt the
processor from its current task and cause it to jump to an interrupt handler,
aka interrupt service routine (ISR). The processor has only one IRQ input,
which is expanded by an 8259 PIC (programmable interrupt controller).
The PC and XT have one PIC, which provides IRQ0-7. IRQ0 and 1 are the timer
tick and keyboard interrupts, respectively. IRQ2 through IRQ7 are available
on the slot bus, for use by peripheral cards.
The AT has two PICs - the primary PIC, which is equivalent to the single PIC
on the PC and XT, and the secondary PIC (also sometimes called the slave PIC).
The third input (IRQ2) on the primary PIC is known as the 'chain' or 'cascade'
or 'slave' interrupt on the AT, because it is the method by which the secondary
PIC issues an interrupt request. The slot bus connection that was IRQ2 on the
PC is replaced by IRQ9 on the AT and later machines (ISA bus).
The two PICs and their interconnection are shown in Figure 2 in the FIGURES
archive.
Each PIC is responsible for prioritising its incoming interrupt requests, and
issuing an interrupt request signal to the processor - either directly (in the
case of the primary PIC) or via the primary PIC (in the case of the secondary
PIC).
Hardware interrupts are registered on the rising edge of the PIC input, which
corresponds to a rising edge of the IRQ line on the slot bus of ISA machines.
This is known as rising edge triggered interrupts. Level triggered interrupts,
particularly active low level triggered interrupts, are more sensible for most
applications, and EISA machines are apparently configurable for either edge-
triggered or level-triggered operation. The MicroChannel Architecture (MCA)
bus, used in IBM PS/2 machines, uses level triggered interrupts.
The PICs are accessed via two I/O locations. The primary PIC appears at I/O
addresses 20h and 21h, the secondary PIC (not present on PC and XT) appears at
I/O addresses 0A0h and 0A1h. The lower address is the command/status register,
the upper address is the interrupt mask register (IMR).
## 6.5 IRQ TO INTERRUPT MAPPING
The default mapping between hardware interrupt requests (IRQs) and interrupts
is set up by the BIOS POST, and is as follows.
┌───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
│ IRQ Int │ IRQ Int │ IRQ Int │ IRQ Int │
├───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┤
│ 0 8 │ 4 0Ch │ 8 70h │ 12 74h │
│ 1 9 │ 5 0Dh │ 9 71h │ 13 75h │
│ 2* 0Ah │ 6 0Eh │ 10 72h │ 14 76h │
│ 3 0Bh │ 7 0Fh │ 11 73h │ 15 77h │
└───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
* Note IRQ2 is not usable directly except on the original PC and XT, which do
not have IRQ8-15. The slot bus connection that was IRQ2 on the PC and XT is
connected to IRQ9 on AT-class ISA machines. The BIOS default handler for IRQ9
(int 71h) invokes the IRQ2 (int 0Ah) handler for backwards compatibility.
I don't know the details of this. (*)
## 6.6 INTERRUPT FLAG, INTERRUPT ACCEPTANCE, INTERRUPT NESTING
When a hardware device requests an interrupt, the PIC tells the processor that
an interrupt is pending. The processor has an 'interrupt enable' flag in the
Flags (F) register, which determines whether the processor will respond to the
interrupt request from the PIC. This flag is cleared and set by the CLI and
STI instructions (respectively) or the disable() and enable() functions or
pseudofunctions, which execute CLI and STI instructions (respectively).
If the interrupt enable flag is clear, the processor will not action the
interrupt request. In this state, the PIC will continue to examine its
inputs, and keep evaluating which interrupt is the highest priority active
interrupt request, leaving its interrupt request line to the processor in
the active state.
Interrupts are prioritised, with IRQ0 (the timer tick) being highest priority
and IRQ7 being lowest priority. IRQ8-15 fit in the gap between IRQ1 (the
keyboard scancode interrupt) and IRQ3 (normally used for COM2). This priority
is determined by control bytes sent to the PICs by the BIOS initialisation code.
It can be changed by reinitialising the PICs but I know of no program that does
this.
When the processor is able to accept the interrupt request, it pushes the flags
and the CS and IP registers onto the stack, and clears the interrupt flag in
the flags register, before allowing the PICs to decide which is the highest
priority interrupt and provide the address of the interrupt vector. The
processor then executes the interrupt handler for the highest priority pending
IRQ. The interrupt routine ends with an IRET, which is like a RETF but also
pops the flags, i.e. 'undoes' the automatic stacking done by the processor when
the interrupt was registered.
During execution of the handler, the PICs continue to evaluate the highest
priority interrupt being requested, and if an interrupt with a higher priority
than the one in progress comes along, they will issue another interrupt request
to the processor. The processor will ignore this request unless the interrupt
handler in progress has explicitly enabled interrupts, by executing an STI or
enable(). In this case, if a higher priority interrupt is pending, the handler
in progress will itself be interrupted, so that the higher priority interrupt
can be serviced. On return from the higher priority interrupt handler, the
lower priority handler will resume.
If during servicing of an interrupt, a lower priority interrupt source comes
along, or the same interrupt is retriggered, the PIC will not interrupt the
processor. Once the handler in progress has terminated, the lower or same
priority interrupt will be actioned.
The PIC knows which interrupt level is in progress, because it triggered the
interrupt itself. But it cannot tell when that interrupt level has been
processed. The interrupt handler has to tell it, via the EOI command, see
section »» 6.28.
As the timer tick has the highest priority, care should be taken to ensure that
it is as short and efficient as possible, because it cannot be interrupted, even
if it enables interrupts. See section »» 6.9 for an exception to this rule.
## 6.7 EMM386 INTERRUPT INTERCEPTION
EMM386 places the 80x86 into virtual 8086 mode and intercepts interrupts at a
hardware level, i.e. through specific features of the 386 and later processors.
This is different from intercepting interrupts at the vector level. The reason
for this behaviour is that several interrupts serve dual purposes - they are
IBM-allocated hardware or software interrupts, but are also Intel-allocated
processor interrupts known as processor exceptions (section »» 6 introductory).
In real mode, the 80x86 will not generate these new internal interrupts, and
behaves like an 8086/8088, 80186, or 80286, but EMM386 must put the 80x86 into
virtual 8086 mode, so that it can use the paging facilities of the 386/486/586
to remap memory, etc. In virtual 8086 mode, these exceptions may occur.
However, DOS and BIOS functions are designed assuming real mode, and do not
expect to be called when these exceptions occur, therefore EMM386 must intercept
these interrupts and when they occur it must determine whether the interrupt is
a real-mode interrupt (in which case it invokes the appropriate interrupt
handler via its vector) or a processor exception (in which case it displays a
friendly message asking whether you want to terminate the program, then usually
locks up the machine regardless :-)
The extra time required for EMM386 to determine the interrupt type adds a
significant amount of overhead to each interrupt, as demonstrated by the
example in section »» 6.8 (software interrupts) and the sample program in
section »» 10.16 (hardware interrupt).
If anyone has more insight into EMM386 and its effects on interrupts, please
let me know. (*)
## 6.8 AVOIDING EMM386 OVERHEAD
Because EMM386 intercepts the interrupt at the hardware level, it can be
bypassed by calling the interrupt handler directly via its interrupt vector,
avoiding the actual INT instruction that will be intercepted by EMM386.
This applies to software interrupts (i.e. function interrupts for BIOS, DOS,
EMS, mouse, etc functions) only. The EMM386 overhead on hardware interrupts
(IRQs) cannot be bypassed.
When doing this manually, care must be taken to ensure that the processor is
in the correct state as expected by the interrupt handler. This involves
ensuring that the interrupt flag is clear. Quite a lot of messing around
is required for a generic solution that preserves all ingoing registers
including the flags (apart from the interrupt flag, of course), as shown in
the following code section, which demonstrates how to call a software interrupt
directly thus bypassing the EMM386 overhead:
-------------------------------- snip snip snip --------------------------------
IntNum EQU 10h ; Interrupt to be invoked
pushf
sub sp,8
push bp
push ax
push ds
mov bp,sp
push WORD PTR [bp+14] ; Take a copy of the flags
mov [bp+12],cs ; Segment of return address
mov WORD PTR [bp+10],OFFSET ReturnPoint ; Offset of same
xor ax,ax
mov ds,ax ; Address interrupt vector table
mov ax,ds:[(IntNum SHL 2) + 2] ; Segment of handler
mov [bp+8],ax
mov ax,ds:[IntNum SHL 2] ; Offset of handler
mov [bp+6],ax
popf
pop ds
pop ax
pop bp
cli
retf ; 'RETF' to handler
ReturnPoint: ; Continue
-------------------------------- snip snip snip --------------------------------
This code section is rather convoluted. It sets up a stack frame as follows:
BP+... Contains
14 Flags at subroutine entry
12 Segment of ReturnPoint
10 Offset of ReturnPoint
8 Segment of handler
6 Offset of handler
4 BP
2 AX
0 DS
-2 Flags (copy of BP+16)
A program to compare the speed of 50000 loops calling video BIOS functions 0Eh
(teletype output) twice, 3 (request cursor position and size), and 8 (read
character and attribute at cursor), first using an INT 10h to call the BIOS
function and then using the above code, gave the following results on my
486DX2-66:
EMM386 Method Time Speed (relative)
Not present Int 10h 838730 100% (normalised)
Not present Above code 991420 84.6%
Present Int 10h 2084600 40.2%
Present Above code 1440275 58.2%
These results show that installing EMM386 slows the system significantly, but
by calling the interrupt directly, some of the overhead is removed. The
speed improvement gained by calling the interrupt instead of issuing an INT
instruction, when EMM386 is installed, is 44.7%. Without EMM386 installed,
calling the interrupt is slower than using INT, because of the messy stack
manipulation.
If anyone has any comments on these findings, please let me know. (*)
## 6.9 LONG TIMER TICK INTERRUPT HANDLERS
A special method may be used if the timer tick interrupt handler must take a
long time. Interrupts may be enabled and an EOI sent to the PIC, making the
PIC think that the timer tick interrupt has been fully serviced. This allows
lower priority interrupts to be serviced and handled in the normal way (but
see section »» 6.9.1), thus preventing problems with the keyboard, mouse, and
serial I/O, but of course this means that another timer tick interrupt could
come along while the current handler is in progress. This will cause the int
8 handler to be re-entered unless the condition is detected using a flag or
'semaphore'. If on entry to the interrupt handler the semaphore is set, the
interrupt handler must send an EOI and exit, or exit by chaining to the
original handler.
Here is an example timer tick interrupt intercepter that hooks into int 8 and
implements this technique. Note that the Int8Sem and TriggerFlag variables
appear in the code segment, to allow them to be accessed via CS (this avoids
wasting time manipulating DS).
-------------------------------- snip snip snip --------------------------------
ASSUME cs:_TEXT ; Current code segment name
ASSUME ds:nothing,es:nothing,ss:nothing
Int8Sem DB 0 ; Int 8 in progress semaphore
TriggerFlag DB 0 ; Flag to trigger long function
NewInt08 PROC far ; Int 8 intercepter
pushf ; Preserve flags
cli ; Make sure interrupts are off
cmp Int8Sem,0 ; Check whether we're already busy
jnz GoOld08 ; If so, don't do anything
; Decide here whether the long function should be performed. If so,
; branch to DoLongFunc, otherwise continue. TriggerFlag must be set
; by some external routine in order to trigger the background function.
; TriggerFlag will be reset to zero by the function when it completes.
cmp TriggerFlag,0 ; Time to perform the function?
jnz DoLongFunc ; If so
; Idle - just jump to old handler
GoOld08: popf ; Fix stack
DB 0EAh ; JMP xxxx:yyyy
Old08Ofs DW 0 ; Vector to original handler - Offset
Old08Seg DW 0 ; Segment
; Time to perform the long function
DoLongFunc: inc Int8Sem ; Set busy flag
pushf ; Simulate stack for an INT
call DWORD PTR Old08Ofs ; Chain to old handler (sends EOI)
sti ; Enable interrupts
push dx ; Preserve
push cx ; Preserve
push bx ; Preserve
push ax ; Preserve
; -- Insert code here to perform your long function. Preserve any other
; registers that you will use, using PUSH and POP. Note the asynchronous
; interrupt handler restrictions still apply (this routine cannot call DOS,
; etc), but this code may take as long as necessary.
; -- Your code goes here
pop ax ; Restore
pop bx ; Restore
pop cx ; Restore
pop dx ; Restore
mov Int8Sem,0 ; No longer busy
mov TriggerFlag,0 ; We have triggered
popf ; Restore flags pushed at start of int 8
iret ; Finally, return to application
NewInt08 ENDP
-------------------------------- snip snip snip --------------------------------
This approach can be thought of as an interrupt-triggered 'branch' to another
section of code. Once this interrupt intercepter calls the old handler to send
the EOI and enables interrupts, it effectively becomes the 'mainline' and runs
'in the foreground', itself being interrupted by other interrupts of any
priority. After completing its function, it may return control to the point
where the interrupt interrupted execution, or it may choose not to do so
(though this requires careful programming). Generally the interrupt handler
restrictions still apply, because you do not know what the machine was doing at
the time that the interrupt occurred.
If used in a TSR, this technique can cause another problem. If another program
hooks int 8, and chains to our interrupt handler using the CALL method so it can
regain control after chaining, that program's interrupt handler may be called
recursively. There are no formal guidelines for writing TSRs (at least, not at
this level of depth) so I don't know how this should be handled. Anyone? (*)
There _are_ TSRs that use this technique - I believe DOS's PRINT program does
this - and it may have implications on int 8 handlers which chain to the
original handler using the CALL method (see section »» 6.31). (*)
Manipulation of semaphores is usually done using indivisible instructions, such
as the XCHG instruction, so that it's not possible for the code to be re-entered
between the semaphore test, and the semaphore set. In this case, that section
of code is uninterruptible, because interrupts are explicitly locked out, so an
indivisible instruction is not required. The explicit CLI instruction should
not be needed, as the interrupt flag is turned off when the processor branches
to the interrupt service routine, but it is good practice to explicitly disable
interrupts here, as there are some programs which intercept int 8 but call the
original handler with interrupts enabled.
## 6.9.1 DANGER OF LONG TIMER TICK INTERRUPT HANDLERS
There is one further problem with this technique. If a lower priority interrupt
was being handled while the int 8 occurred, and is still in progress, it will
not complete and send its EOI until the int 8 handler completes and returns.
Therefore, that interrupt and all lower priority interrupts, are locked out
for the duration of the extended int 8 code. In some cases, it may be useful
to poll the interrupt controller's In Service Register at the start of the int
8 handler, and if any other interrupts are already being serviced, do not do
the extended code. If the interrupt handler _must_ execute the extended code
regardless of whether any lower priority interrupts are being serviced, this
may have an impact on other system functions. Ideas anyone? (*)
## 6.10 INTERRUPT MASK REGISTER
Hardware interrupts may be masked individually via the Interrupt Mask Registers
(IMRs) in the 8259 PIC chips. Each PIC has an 8-bit IMR, in which each bit
corresponds to an IRQ line. Bits 0-7 in the primary PIC's IMR correspond to
IRQ0-7 respectively, and bits 0-7 in the secondary PIC's IMR correspond to
IRQ8-15 respectively. If IRQ2 is masked off in the primary PIC, this masks
off IRQ8-15, as they are signalled by the secondary PIC via this cascade input
on the primary PIC. Therefore, when enabling any IRQ on the secondary PIC
(i.e. IRQ8 through 15), you should also explicitly enable IRQ2 on the primary
PIC.
If the bit in the IMR is set, the interrupt is _masked_ (i.e. disabled). This
is the opposite of the interrupt enable flag in the processor, which is set to
_enable_ interrupts. Setting a bit in the IMR _masks_ (prevents) the interrupt.
The IMR is a read/write register and can be accessed at I/O address 21h (primary
PIC) or 0A1h (secondary PIC). See section »» 6.11 for code examples.
The PIC also contains an interrupt request register (IRR) and an in-service
register. These can be read by issuing the appropriate read-back command to
the PIC and then reading the command/status register at I/O address 20h (primary
PIC) or 0A0h (secondary PIC), see sections »» 6.12 and »» 6.13.
## 6.11 ENABLING AND DISABLING THE TIMER TICK INTERRUPT
Interrupt 8 can be enabled or disabled via bit zero of the interrupt mask
register (IMR) in the primary 8259 PIC, at I/O address 21h. Each bit in
this register controls the correspondingly numbered IRQ, and int 8 is IRQ0.
Setting the bit _masks_ or _disables_ the interrupt, thus the name 'interrupt
_mask_ register'.
Disable interrupts using disable() or CLI around accesses to the IMR. Here are
two sample subroutines to control int 8.
See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-------------------------------- snip snip snip --------------------------------
DisableInt8: ; Destroys AL only
pushf
cli
in al,21h
or al,1
out 21h,al
popf
ret
EnableInt8: ; Destroys AL only
pushf
cli
in al,21h
and al,0FEh
out 21h,al
popf
ret
-------------------------------- snip snip snip --------------------------------
## 6.12 READING THE INTERRUPT REQUEST REGISTER
The IRR in the primary 8259 PIC can be read with the following code fragment.
It returns the IRR value in AL.
-------------------------------- snip snip snip --------------------------------
ReadPIC0IRR: ; Returns IRR in AL
pushf
cli
mov al,0Ah ; Read IRR command
out 20h,al
jmp SHORT $+2
in al,20h ; Read the IRR value
popf
ret
-------------------------------- snip snip snip --------------------------------
If you know that no other software is going to be accessing the PIC, for example
if you are reading the IRR in a loop with interrupts locked out, you can skip
sending the 0Ah to port 20h before every read of the IRR. The PIC remembers
that the IRR is selected to appear on a read of port 20h. So you would send the
0Ah to port 20h at the start of the loop, then just read port 20h every time
through the loop.
The same routine can be adapted to read the secondary PIC, just access port 0A0h
instead of port 20h.
## 6.13 READING THE INTERRUPT IN SERVICE REGISTER
The ISR (In Service Register, not Interrupt Service Routine) in the primary 8259
PIC can be read with the following code fragment. It returns the In Service
Register value in AL.
-------------------------------- snip snip snip --------------------------------
ReadPIC0ISR: ; Returns ISR in AL
pushf
cli
mov al,0Bh ; Read ISR command
out 20h,al
jmp SHORT $+2
in al,20h ; Read the ISR value
popf
ret
-------------------------------- snip snip snip --------------------------------
The In Service Register tells you what other interrupts are 'in service'. For
example, if a serial port interrupt occurred on IRQ4, and during processing of
that interrupt (before the EOI was sent to the PIC), a keyboard interrupt on
IRQ1 occurred, and again during processing of that interrupt, the timer tick
interrupt came along, then the In Service Register would contain 00010011
binary if read inside the timer tick interrupt, indicating that IRQ4, IRQ1,
and IRQ0 are currently 'in service', i.e. their handlers are in progress and
are nested.
If in the above example, the IRQ4 handler completed and sent its EOI to the
PIC before the IRQ1 occurred, its bit would not be set in the In Service
Register.
Because only higher priority (lower-numbered) interrupts can interrupt an
interrupt handler (unless it sends an EOI early, in which case it is no longer
"in service"), any interrupts flagged in the In Service Register must have
occurred one after the other in order, from highest IRQ number to lowest IRQ
number.
The same routine can be adapted to read the secondary PIC, just access port 0A0h
instead of port 20h.
## 6.14 WHEN YOU SHOULD DISABLE INTERRUPTS
Generally, your program should disable interrupts around a sequence of accesses
to an I/O device (such as the PIC, CTC, VGA chips, etc) to ensure that an
interrupt service routine does not come along during your access sequence and
access the chip, disrupting your access sequence. Interrupts should also be
locked out when reading or writing variables that may be being accessed by an
interrupt routine, unless you carefully design your communication with the
interrupt routine so that this is not necessary.
In particular, accesses to peripherals such as the RTC and CRTC (CRT controller)
which have an address register and a data register, must be made with interrupts
disabled, as if interrupts are enabled during such accesses, an interrupt
handler could access the device and change the address register after your code
had set the address but before your code had accessed the register, causing
your code to access the wrong register, with possibly disastrous results - e.g.
an ex monitor :-)
If this results in interrupts being locked out for less than ten microseconds
at a time, this will be acceptable for all normal applications. It might not
be acceptable if the timer tick is running very much faster than usual and low
jitter is needed - see section »» 6.15.
Even when no address register is involved, interrupts should still be locked
out over access sequences. For example, with interrupts enabled, the sequence
in al,21h
or al,1
out 21h,al
looks innocent enough, but what happens if an interrupt is triggered between
the IN and the OUT, and the interrupt routine also modifies the IMR, to turn
on, or turn off, an unrelated interrupt? The interrupt handler will do its
thing, but as soon as it returns, the IMR will be clobbered by an old copy of
the IMR with bit 0 set, breaking the changes made by the interrupt handler.
## 6.15 WHEN YOU SHOULDN'T DISABLE INTERRUPTS
These guidelines apply to DOS and any similar single-tasking operating system.
The maximum length of time that interrupts can safely be locked out for depends
on the operating environment. Although there are no formal guidelines, I would
suggest 100 microseconds as a reasonable limit for good performance, and a few
milliseconds as a sensible upper limit. If high speed timer tick interrupts or
high speed serial communication are being used, the limit will typically be much
lower, depending on the required interrupt rate, to avoid missing interrupts
altogether.
If you require accurately timed interrupt delivery, beware that disabling
interrupts for even a short length of time will cause 'interrupt delivery
jitter' (thanks {JAM} for the term :-) - i.e. occasionally the interrupt will
be delayed slightly. See section »» 6.16 for details.
Locking interrupts out for more than 50 ms continuously may cause missed timer
ticks and problems with the keyboard and network (if present), at least.
If a fast timer tick interrupt (see section »» 8 and subsections) is being used,
or another demanding high speed interrupt such as high speed serial reception,
it is easier to miss an interrupt (or lose data). If a hardware interrupt is
missed because interrupts are locked out, the PIC does not generate an extra
interrupt.
## 6.16 CAUSES OF INTERRUPT DELIVERY JITTER AND FAST TICK LOSS
Interrupt delivery jitter ({JAM}'s term) occurs when interrupt acceptance is
delayed, i.e. there is an unusual or inconsistent delay between the interrupt
being signalled at the hardware level, and the processor starting execution of
the interrupt handler, and the interrupt is serviced late. This happens for
three reasons:
■ Interrupts are locked out (processor's interrupt flag is clear)
■ Equal or higher priority interrupt in progress (see section »» 6.6)
(not normally applicable to the timer tick interrupt)
■ Instruction or DRAM refresh in progress (contributes a very small
amount of jitter, and are unavoidable)
The first reason is the usual reason for interrupt delivery jitter on the timer
tick interrupt (int 8 and int 1Ch). The first and second reasons are the usual
cause of interrupt delivery jitter on other interrupt sources.
For the normal 18.2065 Hz timer tick interrupt, this causes the delivery of
interrupts to be uneven (i.e. to jitter slightly), in either a random or a
partly random, partly cyclic manner. This is not usually a problem, as the
low resolution timer tick is not used when timing requirements are critical.
Interrupt jitter can also affect cases where the timer interrupt itself is not
delayed; for example if an absolute timestamp (see section »» 9) is being used
to timestamp serial data received under interrupt or some other occurrence that
is signalled via an interrupt, if that interrupt is delayed, the timestamp will
reflect the time that the interrupt was serviced, rather than when it was
signalled by the serial chip.
If an interrupt must be actioned within a short length of time, for example a
serial received character interrupt or a fast tick interrupt (used when the
tick rate is increased), delayed interrupt acceptance may result in a missed
interrupt. If this occurs, the PIC does not generate an extra interrupt, in
other words, the whole interrupt is lost, and this results in a cumulative
timekeeping error unless the condition is detected and handled specially (see
section »» 6.17).
There are three main causes of interrupt delivery jitter due to interrupts
being locked out:
■ Real (hardware) interrupts
■ Software interrupts
■ Interrupts disabled while accessing hardware or volatile variables
These causes are now described individually.
## 6.16.1 INTERRUPT DELIVERY JITTER DUE TO REAL INTERRUPTS
Real interrupts include the timer tick (int 8), keyboard scancode (int 9),
serial communication (if enabled) (including serial and bus mouse), RTC (int
70h) (if enabled), and network card (if present) interrupts. The handlers for
all of these interrupts (except the timer tick) should re-enable interrupts
quickly so that higher priority interrupts including the timer tick are not
delayed for long, but some handlers do not enable interrupts because of bad
design or deliberately, due to other considerations. Also EMM386 imposes extra
overhead during interrupt acceptance; during this time another interrupt cannot
be accepted, I think. (*)
For example, if a network card interrupt handler on IRQ3 (for example) does not
enable interrupts, and this interrupt is invoked on every network data block
received by the machine, then every time a block of data is received, interrupts
are locked out for, perhaps, 100 us. If a timer tick interrupt is signalled
during this time, its acceptance will be delayed for up to 100 us, and interrupt
delivery jitter occurs.
Also, {JAM} points out that screen savers, which typically hook int 8, often
clear the whole screen with interrupts disabled, resulting in a very long
int 8 every once in a while when the screen saver 'kicks in'. Many other
programs also intercept int 8 and will increase the amount of time occupied
by each int 8 and/or by occasional int 8 invocations - network software uses
int 8 as a timebase for timeout detection {JAM}, mouse drivers use it, and
lots of pop-up and non-pop-up TSRs also use int 8.
{TOR} points out that some BIOSes can also be the culprit:
> The usual BIOS implementation of the keyboard interrupt and the floppy drive
> interface are among the worst for blocking interrupts. I have actually
> seen a keyboard driver [int 9 handler] issue its buffer full 'beep' with
> interrupts locked out...
## 6.16.2 INTERRUPT DELIVERY JITTER DUE TO SOFTWARE INTERRUPTS
Every time a software interrupt is issued, the processor disables interrupts
before executing the interrupt handler. Well-behaved software interrupt
handlers re-enable interrupts immediately on entry, but not all software
interrupt handlers are well-behaved. In any case, there is a short length of
time during which interrupts are disabled, and this time is lengthened if
EMM386 is installed, because EMM386 intercepts the interrupt at a hardware
level, and has to work out whether the interrupt is software-generated or is
processor-generated, because several low-numbered interrupts are both processor
exceptions and software interrupts.
Therefore, every time your program or any code called by your program issues a
software interrupt, interrupts are locked out for a short time, and possibly a
long time if the interrupt handler is badly written or if several programs
(e.g. TSRs) have intercepted that interrupt and many interrupt chains are
performed before the request reaches its actual handler.
Other software interrupts which may spend a significant length of time with
interrupts locked out are:
■ Screen scrolling via the BIOS
■ Hard drive read, write, and seek accesses (possibly)
■ Network accesses
■ Mouse driver function calls
■ EMS function calls
■ XMS function calls
■ Potentially, any code you did not write yourself!
## 6.16.3 INTERRUPT DELIVERY JITTER DUE TO HARDWARE ACCESSES
Often, interrupts must be disabled manually, using CLI, around access sequences
to hardware devices (see section »» 6.14) or accesses to volatile variables
that may be modified by a hardware interrupt handler. If an interrupt is
flagged during the short time that interrupts are locked out, it will be
delayed until interrupts are re-enabled, causing interrupt delivery jitter.
There are also other reasons why software might disable interrupts, usually
(but not always) for short periods only. If your program requires very low
jitter, it will probably have to do everything itself, because it cannot call
any normal BIOS or DOS functions!
## 6.16.4 AVOIDING INTERRUPT DELIVERY JITTER
If your application must run with a very fast timer tick interrupt, or must
have very low interrupt jitter for whatever reason, it must avoid all of the
causes of interrupt jitter described in sections »» 6.16 through »» 6.16.3.
Interrupt jitter due to instruction execution (i.e. the interrupt cannot be
accepted until the instruction in progress is completed) is unavoidable, but
could probably be reduced by using short instructions and avoiding prefixes.
Other causes of interrupt delivery jitter must be avoided for good results.
This comes down to the following restrictions:
■ Disable all hardware interrupt sources via the PIC(s)
except the interrupt source you are using
■ Do not issue software interrupts
■ Do not call code over which you do not have control
■ Do not chain to the original interrupt handler
■ Do not disable interrupts using CLI at all
■ Run the program without EMM386 if possible
Following these guidelines should ensure that interrupts are never locked out
due to a hardware interrupt, software interrupt, or deliberate execution of a
CLI instruction.
Disabling IRQ1 (keyboard scancode) will disable the keyboard, of course, and
disabling serial interrupts may disable the mouse. Most other interrupts are
not active in the background (e.g. the floppy disk interrupt is only active
when a disk access is in progress) and should be unaffected.
If you are using int 8 and the interrupt rate is fairly slow, you may choose to
chain to the original int 8 handler, because this will not cause jitter as it
only executes _after_ the interrupt has been registered. However, this could
cause problems if TSRs and/or drivers are using int 8 and occasionally do
something nasty such as using the long tick interrupt handler technique
described in section »» 6.9, where they gain full control of the machine for
a while. In short, if you chain to the original handler, you are giving
execution to code over which you have no control, so if jitter is critical,
you do so at your own risk!
If you need very fast interrupts or very low interrupt jitter, be very careful
about what you do and who you call - you may need to do everything yourself to
avoid interrupt latencies!
I don't know the details of EMM386 and its effects on interrupt jitter.
For example, it may internally trap some privileged instructions, and delay
interrupts while processing these instructions. If anyone knows the details,
please let me know! (*)
## 6.17 DETECTING INTERRUPT DELIVERY JITTER AND MISSED FAST TICK INTERRUPTS
Interrupt delivery jitter on int 8 can be detected by reading CTC channel zero
on entry to your interrupt handler and looking at the amount of variation from
the highest raw value read, or the expected raw value (assuming that the reload
value is known). See the sample program in section »» 10.16.2 for an example
of this technique. If your application will be sensitive to interrupt jitter,
you should incorporate this type of check, and if jitter is excessive, perhaps
advise the user that there is a problem and he/she should ascertain which driver
or TSR is causing the problem and get technical help to fix it if possible.
If a fast timer tick rate is being used, a missed interrupt can be detected by
using another CTC channel as a reference, providing that the CTC channel is not
required (and will not be touched) for any other purpose. I would suggest using
channel two, which is normally used for speaker audio generation. You would set
channel two to a large divisor (e.g. 65536) and mode two, and make sure that
nothing else touched it - i.e. disable, or at least don't use, the BIOS video
function that emits a beep (int 10h with AX = 0E07h), and possibly hook into the
keyboard subsystem to prevent the beep when the type-ahead buffer gets full.
Your fast tick interrupt routine would read a timestamp from CTC channel two
to determine how many fast ticks have been missed and adjust its behaviour
accordingly. This approach would prevent the cumulative error, but would not
fix the 'jumpiness' or 'jitter' of the timekeeping.
## 6.18 DISABLING INTERRUPTS FOR LONGER THAN ONE TIMER TICK
In some applications, you may choose to disable interrupts for longer than the
recommended maximums in section »» 6.15. You can also selectively disable the
timer tick interrupt and any other hardware interrupt source, via the PIC IMR
(section »» 6.11). You will have to deal with the implications of doing this,
however.
While interrupts are disabled via the processor's interrupt flag, interrupts
accumulate, so as soon as the interrupt flag is set (via a POPF or STI), {JAM}
says: "the program does not regain control until ALL outstanding interrupts are
processed, including interrupts that happen while the outstanding ones are being
handled. On networked machines, that time may be in milliseconds!"
## 6.19 DISABLING INTERRUPTS FOR LONG PERIODS OF TIME
If it is necessary to disable interrupts for a long period of time, causing
timer ticks to be missed, be aware that doing this is likely to sabotage any
network software on the machine, and will also break the mouse driver while
interrupts are locked out. You should take the following precautions.
Don't start the section of code where interrupts are locked out, until the
floppy disk drive motors have all turned off. Check the byte at low memory
location 0040:003F. If it's nonzero, one or more floppy disk drive motors
are active. Wait until it is zero.
Assuming you don't want the machine to lose time, you can either read CTC
channel zero regularly and watch for a borrow and increment the BIOS timer
tick count when that occurs (remember the wrap-around and the midnight flag),
or upon completion of the no-interrupt section of code, read the RTC and
calculate and store the appropriate timer tick value. This also requires
setting the midnight flag if appropriate.
Generally if you want to disable interrupts for _that_ long, you will be
running the program on a dedicated machine, and you may not be too concerned
about loss of time. In this case, since you have control of the machine that
the software will be running on, you could install the ATRTC driver, see
section »» 3.3, which removes the dependency on the BIOS timer tick for
timekeeping. The other problems still remain, however.
## 6.20 OVERHEAD OF AN INTERRUPT
When an interrupt is accepted, the processor branches to the interrupt handler.
On modern processors, this causes the prefetch queue to be flushed, wasting a
small amount of time. Of course the prefetch queue is being flushed all the
time, by branches and jumps and calls, etc, so this is not a major problem.
The prefetch queue will be flushed when the interrupt is accepted, and again
when the interrupt handler returns with an IRET.
A bigger problem is code and data caching. {JAM} Because this caching is done
in blocks, the interrupt may cause wanted code to be flushed from the cache,
to make room in the cache for the interrupt handler code, wasting considerable
time in reloading the cache when the interrupt completes.
There is nothing that can be done about either of these problems.
{JAM} In protected mode, interrupt overhead is very much higher, because of
the privilege changes, mode switches, etc that are involved. Interrupt
overhead on a 386SX-25 is in the order of a few hundred microseconds.
I assume this refers to a real-mode interrupt handler being used with protected
mode code. If the interrupt handler operated in protected mode, or was a dual-
mode interrupt handler (could operate in either mode), this overhead would not
exist, presumably. If anyone has more detailed information on this subject,
please let me know. Also any detailed information on what EMM386 does to
interrupts and how much overhead it imposes, and if there is any way to bypass
it, would be great. (*)
Tor Sjowall {TOR} also mentions an additional source of interrupt overhead -
the stack switch that DOS does on hardware interrupts if you have a line
'STACKS=X,Y' in CONFIG.SYS. I don't know how this stack switching works, or
at what level it operates. Please let me know if you can help. (*)
## 6.21 EFFECT OF BACKGROUND INTERRUPTS
The timer tick interrupt is normally permanently enabled, and from the point of
view of the code being interrupted, it introduces a 'gap' in time, at regular
intervals (assuming interrupts are enabled). You could imagine that the
processor gets abducted by aliens in a UFO (if you had a vivid imagination :-)
One moment it's executing your main routine, minding its own business, then
suddenly it is taken away and made to do something else, then when it returns
to where it was, it continues normally, without even knowing that it had been
doing something else, except that some time has elapsed. Excuse the analogy.
Your foreground code is constantly being interrupted without its knowledge.
{JAM} explains this quite nicely as follows:
"The IBM PC has a constant active background process that results in a small gap
in any loop. This becomes magnified when programs are compiled for protected
mode. Moreover, the standard hardware can add additional gaps. Most often
these gaps are under our control. Finally, when connected to a network, many
types of background activities can happen, most of which we cannot predict and
are beyond our control. Whenever we design a program to function on networked
machines, we must remember that these background processes are in effect and we
must take them into account. For example, when we poll a device, we must be
aware that there will be missing time slices from that polling".
Unless specifically enabled by your program, the only interrupt sources likely
to be operating regularly while the machine is idle, are int 8 (timer tick),
int 9 (keyboard scancode), and interrupts for the serial mouse or bus mouse,
and network card interrupts.
## 6.22 SAFE CONTROL OF INTERRUPTS
When you access hardware devices (reading or writing the CTC registers, for
example), you could disable interrupts around the access, like this:
-------------------------------- snip snip snip --------------------------------
void write_some_registers(void) { /* Unsafe method! */
disable(); /* Or asm cli */
outportb(port1, value1); /* whatever you need to do */
outportb(port2, value2); /* whatever you need to do */
enable(); /* Or asm sti */
return;
}
-------------------------------- snip snip snip --------------------------------
This assumes that the function that called this function was operating with
interrupts enabled, and wants them re-enabled when this function has finished
talking to the hardware. This may not be the case!
For example, the function that called this function may already be doing
something critical which requires interrupts to be locked out, and remain
locked out continuously during the call to our write_some_registers() function.
The safe way to handle this is as follows:
-------------------------------- snip snip snip --------------------------------
void write_some_registers_safely(void) {
asm pushf;
asm cli; /* or use disable() */
outportb(port1, value1); /* whatever you need to do */
outportb(port2, value2); /* whatever you need to do */
asm popf;
return;
}
-------------------------------- snip snip snip --------------------------------
Here, we push the flags register onto the stack before disabling interrupts,
then pop the flags register back once we have finished. This ensures that
interrupts are locked out during our hardware manipulation, and also that
the correct state of the interrupt flag is restored once we have finished.
If interrupts were enabled on entry, the popf sets the interrupt flag ON, and
the function effectively only disables interrupts for the minimum time, i.e.
between the disable() (CLI) and the popf.
If interrupts were disabled on entry, the popf sets the interrupt flag OFF,
which it already was from the disable() (CLI instruction). Thus the routine
_never_ enables interrupts.
This simple technique ensures that the routine can be safely used in either
situation - either interrupts allowed, or interrupts not allowed.
According to an article by James Ralph (jim@grc.com) in PC Magazine, September
13 1994, page 340, there is a bug in some 286 processors which causes the popf
instruction to briefly enable interrupts. The workaround proposed by James is
to use an IRET (which presumably does not suffer from this bug) instead of a
POPF (an IRET pops IP, CS, and the flags). This approach requires that you push
CS and IP onto the stack first. The example given by James is similar to this:
-------------------------------- snip snip snip --------------------------------
pushf ; Keep flags including interrupt flag
cli ; Disable interrupts
; Do critical stuff in here - interrupts are locked out
push cs ; Have flags on stack, now push CS
call NEAR AnIRET ; CALL pushes IP, IRET pops IP, CS, flags
; Continue with the main function - interrupt flag is now restored to its
; original value on entry to the function
ret ; End of the function
; Put the IRET somewhere in the code segment - it can be used by multiple
; instances of the above code.
AnIRET: iret
-------------------------------- snip snip snip --------------------------------
Another way to handle this would be:
-------------------------------- snip snip snip --------------------------------
pushf ; Keep flags including interrupt flag
cli ; Disable interrupts
; Do critical stuff in here - interrupts are locked out
push cs ; Have flags on stack, now push CS
push WORD PTR cs:RetAdr ; Push a value for IP
iret ; Pops IP, CS, and flags
RetAdr DW RetPoint ; Offset to 'return' to
RetPoint:
; Continue with the main function - interrupt flag is now restored to its
; original value on entry to the function
-------------------------------- snip snip snip --------------------------------
I have not taken this precaution in the sample code, because I'm lazy, but you
probably should use this method unless your program will never be run on 286
machines or is 386/486/586-specific.
## 6.23 TIMER TICK INTERRUPT HANDLER GUIDELINES
Note that these comments also apply to other asynchronous interrupts, such as
the keyboard interrupt (int 9) and the serial and parallel port interrupts.
For full details, find a DOS reference that discusses ISR programming and TSR
techniques.
Both int 8 and int 1Ch are asynchronous hardware-triggered interrupts (although
int 1Ch is actually software-generated). See section »» 6.35 for a discussion
of the differences in usage between int 8 and int 1Ch.
There are major restrictions on what can safely be done inside an asynchronous
interrupt handler, because when it is invoked, the hardware and software state
of the machine is not known.
For example, DOS may be in the middle of writing to a printer port, waiting for
user input, or processing a disk I/O request, or the BIOS may be busy scrolling
the text screen, plotting a pixel, beeping the speaker, or programming the DMA
controller ready to transfer a sector of data from a floppy disk. Also, a C
library function that uses static variables may be in progress. In fact, more
than one of these 'levels' may be busy. For example, an fopen() call could be
in progress, which called DOS, which called the BIOS to read a sector from a
floppy disk, which is busy programming the DMA controller. Therefore, all of
these software and hardware blocks are busy.
In general it is best to limit the functions performed by an interrupt handler
to minimal hardware manipulation, and use a shared variable interface with the
main program, whenever possible. Be careful to make your main program aware of
the interrupt routine - programs do not normally expect certain variables to
change magically of their own accord. Use the 'volatile' keyword when declaring
these variables and use disable() and enable() (CLI and STI) around any critical
code sections.
Also, if your timer tick interrupt handler may use a lot of stack space you
should consider switching to another stack. This is much easier if the
interrupt handler is written in assembler.
Keep int 8 and int 1Ch routines as short and fast as possible to reduce delays
imposed on other interrupt sources.
## 6.24 ACCESSING HARDWARE DEVICES IN AN INTERRUPT HANDLER
Asynchronous interrupts are 'background' processes. It is not always safe for
them to access hardware devices, because the 'foreground' processes - the main
body of your program, or a function (e.g. DOS, BIOS, EMS, XMS, mouse, network,
etc) called by your program - may be in the process of accessing the device, or
may be expecting the device to remain in a certain state.
Often, foreground accesses consist of reading and/or writing a few I/O locations
in sequence, as with the CTC. To make things safe for your interrupt routines
and for TSRs, when you access devices in this way in your foreground code, you
must _ALWAYS_ disable interrupts around the sequence. This applies to devices
such as the RTC, CTC, PICs, DMA controller, VGA ASICs, etc.
Many hardware devices can be accessed (carefully) by an interrupt routine. This
may be because they are not normally accessed in the foreground, or because the
interrupt routine uses a part of the device that is not used by the foreground
processes, or because the interrupt routine's accesses do not conflict with the
foreground process's accesses.
If you know enough to access the hardware directly, you will know when and how
the foreground processes will access the device, so you can figure out what your
interrupt routine can and cannot do safely with that device. Reading the CTC
in an interrupt handler is always safe, providing that your foreground program
is well-behaved (always disables interrupts around access sequences) and always
reads the appropriate number of bytes from the data register, so the lobyte/
hibyte flag remains in sync (see section »» 7.17).
## 6.25 CALLING DOS AND BIOS IN AN INTERRUPT HANDLER
Much of the BIOS, and all of DOS, is not re-entrant, and therefore cannot safely
be called from an asynchronous (hardware) interrupt handler, because it might
have been busy (i.e. in progress) when the interrupt occurred.
None of the applications in this document require DOS or BIOS to be called from
an asynchronous interrupt handler. If you need to do this, get a reference on
TSR programming, as it is non-trivial!
## 6.26 CALLING C LIBRARY FUNCTIONS IN AN INTERRUPT HANDLER
Many C library functions call DOS or BIOS functions, and are subject to the same
restrictions. Also, some C library functions may not be re-entrant for other
reasons - for instance, they may use global or static variables, or allocate
memory which is in the process of being allocated to the foreground program.
Check your compiler's library reference or programmers' guide for information
about TSR considerations and re-entrancy of library functions.
## 6.27 RE-ENTRY OF INTERRUPT HANDLERS
Generally hardware interrupt handlers are not re-entered, i.e. are not restarted
during their execution, because they do not send an EOI (see section »» 6.28)
until they have completed, and then interrupts are locked out (see section
»» 6.29). There is one exception to this rule, which applies when a TSR uses
the long timer interrupt technique described in section »» 6.9. This technique
can also be used with the keyboard scancode interrupt, when a TSR pops up using
that interrupt, but see section »» 6.9.1 for a potential problem.
In these cases, the interrupt handler of a foreground program may actually be
re-entered during processing, if it chains to the original handler using the
CALL method (see section »» 6.31), because the original handler (which is the
TSR's handler) can issue an EOI, allowing the entry part of the interrupt
handler to be re-entered. The TSR's own interrupt handler will be aware of
re-entry considerations, because the TSR will be causing them, but an interrupt
handler in a foreground program may not have been designed with this in mind.
They probably should be designed to support this technique. See section »» 6.9
for more details.
If the code in the interrupt handler is inherently non-reentrant, this can be
handled using a semaphore to detect re-entrance, as described in section »» 6.9.
If the semaphore is set at the start of your handler, it should probably chain
to the original handler using the JMP method without performing its normal
function. In some cases it is possible that the semaphore would become set and
would never clear. Hopefully nobody is even reading this stuff, as it is
excessively boring. Yibble yibble yobble yoo, I am a fence post. It is time
for my pill - I have to take one every 54.9254 milliseconds.
## 6.28 THE 'END OF INTERRUPT' SIGNAL
Interrupts 8 to 15 (corresponding to IRQ0 to 7) and interrupts 70 hex to 77 hex
(corresponding to IRQ8 to 15) are generated by hardware devices. An interrupt
service routine for these interrupts must inform the 8259 PIC(s) when the device
which generated the interrupt has been serviced, so that the PIC can reset its
priority structure. This is done using a non-specific EOI (end of interrupt)
command to the PIC. For int 8 to 15 (IRQ0 to IRQ7), a single EOI is used:
outportb(0x20, 0x20); in C, or
mov al,20h
out 20h,al in assembler.
For int 70 hex to 77 hex (IRQ8 to IRQ15), two EOIs are used:
outportb(0xA0, 0x20);
outportb(0x20, 0x20); in C, or
mov al,20h
out 0A0h,al
out 20h,al in assembler.
For IRQ8 through IRQ15, the EOI is typically sent to the secondary PIC first,
as in these examples, though I don't believe there is any significance to the
order in which they are sent.
Normally the EOI is sent at the end of the interrupt routine just before the
IRET instruction. See section »» 6.29 for interrupt control details.
You can use the specific EOI command if you prefer - the value is 60 hex plus
the IRQ number within the PIC, for example to send a specific EOI for IRQ4:
outportb(0x20, 0x64);
and to send a specific EOI for IRQ11:
outportb(0xA0, 0x63); /* IRQ11 is input 3 on the second PIC */
outportb(0x20, 0x62); /* The chain IRQ is IRQ2 */
## 6.28.1 LEVEL TRIGGERED INTERRUPT RESET
IBM PS/2 machines that use MCA (Microchannel Architecture) buses have level
triggered interrupts. This poses a problem for the timer interrupt - how to
clear the timer interrupt request. I have no formal documentation on this, but
I saw the following note in an article by Bob Smith (bobs@access.digex.net) in
mid November 1995:
> On an IBM Micro Channel Architecture system, the timer tick handler in the
> BIOS sets the Clear IRQ0 bit (bit 7 in I/O port 61h). Without this, the
> hard disk won't work. This might, in fact, apply to all level-triggered
> interrupts in general, but I found out about setting that bit before having
> to experiment any further.
So it appears that the timer interrupt must be explicitly acknowledged and
cleared on an MCA system, in addition to sending the EOI. This makes sense
as there is no other way for the level to be reset to deassert the interrupt
request in a level triggered interrupt system. There may be some similar
requirement for an EISA system running in level triggered interrupt mode.
Any more information would be welcomed. (*)
This also has implications when int 8 is operated at a higher rate, because the
int 8 intercepter would have to manually acknowledge the interrupt, in addition
to sending the EOI, every time it didn't chain to the original int 8 handler.
This may mean that a standard int 8 handler for a fast timer tick interrupt
(see section »» 8) will not work on a PS/2.
## 6.29 ENABLING AND DISABLING INTERRUPTS IN AN INTERRUPT HANDLER
On entry to an interrupt handler, processor interrupts are disabled (as if a
disable() or CLI had been issued). Normal practice is to enable interrupts as
soon as possible, perform processing, disable interrupts again, issue an EOI
if applicable (see section »» 6.28), and return from interrupt. However, int
8 is the highest priority interrupt source, and until the EOI is sent, no other
interrupts will get through (except NMI of course) so there's no need to enable
interrupts during int 8 or int 1Ch processing, unless you hare re-ordered the
interrupt priorities.
The EOI command is sent to the PIC(s) at the end of the interrupt handler. For
interrupt handlers which enable interrupts during processing, it is normally
wise to disable interrupts using disable() or CLI just before issuing the EOI,
so that another equal or lower priority interrupt does not occur after the EOI
but before the IRET. Typical coding would be:
IntHandler: sti
push ax
push other registers
; ... interrupt processing here
pop other registers
IntFinished: mov al,20h
cli
out 20h,al
pop ax
iret
This particular consideration does not normally apply to int 8 handlers as they
are normally the highest priority interrupt and do not need to enable
interrupts during their operation. If an int 8 handler does enable interrupts,
however, the above precaution should be taken.
## 6.30 STACK USAGE AND STACK CHECKING IN AN INTERRUPT HANDLER
Stack usage (function nesting depth) must be kept to an absolute minimum unless
your interrupt handler performs a stack switch to a local stack. Normally, you
will be using the stack of whichever program was active at the moment that the
timer tick occurred, and you don't know how much spare room there is in that
stack.
For interrupt handlers written in C, don't go allocating automatic strings or
arrays! Declare any local variables static if possible. If your compiler has
stack checking ON by default, and isn't too bright, you may need to turn stack
checking OFF for all interrupt handlers, and for any functions that may be
called by them, using the appropriate compiler directive.
The directives for Borland C++ 4.0 (and probably 3.1 as well) are:
#pragma option -N- turn OFF stack checking
#pragma option -N turn ON stack checking if perviously enabled
These can be placed around the whole function (or group of functions) that
are to be compiled without stack checking, or just around the first line of
the function (that gives the return type, function name, and parameters).
That information was kindly sent by Michael Mauch (mauch@uni-duisburg.de) who
mentions another problem he found with BC++ 4.0. He had an _inlined_ function
that was called by an interrupt handler. Both the interrupt handler and the
inlined function were declared with stack checking off. When he temporarily
disabled inlining, during debugging, the compiler generated a stack check in
the called function! The moral of the story is you can't always trust your
compiler :-)
If anyone can provide details of stack checking directives for other compilers,
please let me know. (*)
## 6.31 CHAINING TO THE OLD INTERRUPT HANDLER
Most interrupts have a default handler. Before your program takes over control
of an interrupt, it must store the contents of the interrupt vector, which will
be a far (i.e. segment and offset) pointer to the original handler, and which
must be restored when your program terminates (see section »» 6.3).
Often your replacement interrupt handler will need to use the original handler.
This is called _chaining_ to the original handler of the interrupt, and is done
through the pointer that your program stored when it intercepted the interrupt.
Sometimes your replacement interrupt handler will always chain to the original
handler, and sometimes chaining is done conditionally, i.e. when required or
when appropriate.
When chaining to an original interrupt handler, remember that the original
interrupt handler was written to assume that it _was_ the handler for this
interrupt source. Sometimes this requires a little care to make sure that it
will operate properly if called by your replacement handler. For example,
the BIOS int 8 handler issues an EOI command (see section »» 6.28) every time
it is called, so if your interrupt handler chains to the BIOS's interrupt
handler, it should not issue the EOI itself. It must issue the EOI if it
does _not_ chain to the BIOS's interrupt handler, however. Also, the original
interrupt handler will probably assume that interrupts will be disabled when
it is invoked, as this is the case when it is invoked directly, so you must
ensure that interrupts are disabled before chaining.
There are two ways of chaining to the old handler - you can bury her, burn her
or dump her. I mean, you can call it, or you can jump to it. Call it when
your interrupt handler needs to regain control after the old handler has been
invoked. Jump to it when you do not need to get control back, as this uses
less stack space and is tidier. In the Thames.
Remember that the processor pushes the flags, CS, and IP (in that order) when
it accepts an interrupt, and an IRET (which is the way most handlers will exit)
pops these registers back again. Therefore if you chain to a handler with a
CALL, you must push some flags first, then use the far form of CALL, so that
the IRET will return correctly.
Chain_Call:
; ... Initial interrupt processing here
pushf ; Simulate stack for an INT
cli
call FAR OldIntPtr ; Call old handler
; ... More interrupt processing here
iret
Chain_Jump:
; ... Initial interrupt processing here
cli
jmp FAR OldIntPtr ; Call old handler
Note that some interrupts, specifically int 1Ch, do not require chaining, as the
default handler is just an IRET. But see sections »» 6.33 and »» 6.35.
See the interrupt handlers in the sample programs for more details.
If you use the CALL method to chain to the old interrupt, in an int 8 handler,
beware that there may be a TSR using the Long Tick Interrupt technique described
in section »» 6.9, which will send an EOI but not return, thus causing your
interrupt handler to be re-entered while it was part-way through execution.
You probably should design the handler to support this possibility.
If you use the JMP chaining method, this consideration does not apply.
## 6.32 WRITING INTERRUPT HANDLERS IN ASSEMBLY LANGUAGE
Here are some guidelines and warnings that you should heed if you are coding
an interrupt handler in assembly language.
On entry to the interrupt handler, the only registers that will be known are
CS and IP. DS is undefined. You must preserve any other registers that you
modify, except the flags (which will be restored by the IRET).
Also see sections »» 6.23 to »» 6.26 for restrictions on what may be called
from, and done inside, your interrupt handler.
## 6.32.1 ASSEMBLY LANGUAGE INTERRUPT HANDLERS: ACCESSING VARIABLES
If your interrupt handler must have access to memory variables, such as flags,
structures or buffers used to communicate with other parts of your program,
there are three main ways to do this:
■ Common code and data segment (COM files; tiny model), access with CS
■ Put variables in code segment and access them using CS
■ Put variables in data segment and set DS so you can access them.
The first approach is used in single-segment COM-files (also known as tiny
model) in which the code and data segment-paragraphs are the same. In these
programs, CS will already address the segment (because the interrupt handler
is in the same segment as the data), so you can access variables using CS.
This is done via the ASSUME directive, which tells the assembler what segment
each of the segment registers is supposed to contain. The directive:
ASSUME cs:_TEXT,ds:nothing,es:nothing,ss:nothing
tells the assembler that only the CS register is known at the moment, and that
CS addresses the _TEXT segment. You would change the name to whatever segment
you use for your single segment. This directive should appear before the
interrupt handler. The ASSUMEd registers remain in effect until modified by
another ASSUME directive.
The above ASSUME directive tells the assembler that only the _TEXT segment is
addressable, and that every access to a variable in that segment will require
a CS segment override prefix. You need not explicitly code the CS override on
every instruction - the assembler takes care of this automatically (unless
you're using A86 :-) But be very careful with string instructions, because
they don't make references to data objects, and an explicit segment override
may be required. For example, if only CS is ASSUMEd:
mov ax,SomeVariable ; This will generate a CS
; override and will work,
mov si,OFFSET SomeTable ; Point to start of table
lodsw ; Uh-oh! No override will
; be generated on this!
lodsw cs: ; This will work
The second method is used with multiple segment programs. The variables are
placed in the code segment, typically near to the interrupt handler, and are
accessed in a similar way. An ASSUME directive should be used to tell the
assembler that CS is the only known segment register, and that it addresses
the code segment (_TEXT or whatever). The trouble with this method is that
the main program has to access those variables in the code segment, which is
messy.
This second method is most often used in large assembly language programs.
The third method is the method used in C programs and most high level programs,
where placing variables in the code segment is frowned upon and/or impossible.
The variables are placed in the data segment, as normal. This requires that
somehow, a segment register (typically DS) must be loaded with the appropriate
segment at some point in the interrupt routine, before those variables are
addressable. This also requires that the segment register (e.g. DS) is pushed
at the start of the interrupt handler and popped again before it terminates.
Again, an ASSUME directive should be used to tell the assembler what segment
registers point to what. Here is a sample code fragment:
-------------------------------- snip snip snip --------------------------------
DATA SEGMENT
SomeVariable DW 0 ; Some variable, used by int. handler
AnotherVar DW 0 ; Another variable, ditto
DATA ENDS
CODE SEGMENT
ASSUME cs:CODE,ds:nothing,es:nothing,ss:nothing
MyIntHandler PROC far
pushf ; Preserve flags
push ax ; Preserve register
push ds ; Preserve DS
mov ax,SEG DATA ; Get data segment to AX
mov ds,ax ; Move it to DS
ASSUME ds:DATA ; (CS, ES and SS are unchanged)
mov ax,SomeVariable ; Get some variable
add ax,AnotherVar ; etc, you get the idea...
; -- More code here
pop ds ; Restore DS
ASSUME ds:nothing ; Cannot address anything useful with DS
pop ax ; Restore AX
popf ; Restore flags
DB 0EAh ; JMP xxxx:yyyy
OldIntOfs DW 0 ; Vector to original handler - Offset
OldIntSeg DW 0 ; Segment
MyIntHandler ENDP
-------------------------------- snip snip snip --------------------------------
I suggest using tiny model for assembly language programs (avoids segment
register setting in the interrupt handler), or for assembly language programs
in other models, place the variables in the code segment, and for C programs,
place them in the data segment. This is a matter of personal preference,
though.
## 6.32.2 ASSEMBLY LANGUAGE INTERRUPT HANDLERS: STARTING CONDITION
The interrupt flag in the flags register will be clear (i.e. interrupts
locked out) unless a badly behaved interrupt handler has chained to your
interrupt handler but left interrupts turned on. If you are doing critical
hardware access in your handler, you may want to issue a CLI just in case.
This should not apply to int 8, as it is the highest priority interrupt and
should never be interrupted (except by an NMI!)
You may have noticed that I pushed and popped the flags in the sample code
in section »» 6.32.1. This is probably not necessary in such a case as the
original flags are popped by the IRET at the end of the old interrupt handler
that is being chained to via the JMP instruction (see section »» 6.31), but I
think it's wise to make sure that the chained interrupt handler starts with
the same flags that it would have had if our interrupt handler was not present.
The direction flag will probably be clear, but DON'T COUNT ON IT! If you do
any string manipulation in your interrupt handler, be sure to include a CLD
instruction to ensure that the direction flag is known. Forgetting this
precaution is an open-armed invitation to subtle intermittent bugs.
## 6.32.3 ASSEMBLY LANGUAGE INTERRUPT HANDLERS: PRESERVE THE REGISTERS
Of course, your interrupt handler must not modify any registers - use PUSH and
POP to preserve the old values in registers if you need to use the registers
for something else.
Watch out for instructions that modify unexpected registers - for example
the 16-16-32 MUL instruction modifies DX; even if you don't _use_ the high
word of the result, DX will still be modified.
## 6.33 USING INTERRUPT EIGHT IN A TSR
You must intercept int 8 when speeding up the timer tick (see section »» 8).
Int 8 can also be used by TSRs which want a regular interrupt source. TSRs
should not use int 1Ch, though some do - see section »» 6.35.
On installation, your TSR should obtain the contents of the int 8 vector using
getvect() or DOS function 35 hex, and store it, then replace the interrupt
handler with its own handler.
Every time the TSR's int 8 handler is called, it must chain to the old interrupt
handler, usually by jumping to it, as described in section »» 6.31. Your TSR
then has a regular 54.9254 millisecond interrupt source. If a foreground
program reprograms the timer tick for a faster rate (see section »» 8), calls
to your int 8 handler may be unevenly spaced. In the worst case, it is
possible for int 8 invocations to be spaced as closely as 27.4627 ms, half the
normal spacing, and as far apart as 82.3881 ms, 1.5 times the normal spacing.
Over a period of time the interval between int 8 calls should average out to
54.9254 ms, though. See sections »» 6.23 to »» 6.26 for details of restrictions
and techniques for interrupt handlers.
If your TSR can be uninstalled from the command line (a useful feature), the
original int 8 vector contents must be restored, but before restoring vectors
when uninstalling, ensure that the int 8 vector, and the vectors for any other
interrupts your TSR intercepts, are currently pointing to the handler in the
installed copy of the TSR. If they do not, one or more TSRs have been loaded
after your TSR, and it is not safe to uninstall your TSR because restoring the
interrupt vectors will unhook the other TSRs and sabotage their operation. In
this case, you must advise the user that the TSR cannot be uninstalled as other
TSRs have been installed above it.
I believe there is a package called Tesseract, or maybe AMIS, written by Ralf
Brown of the Interrupt List fame, which provides a general TSR template and
also permits compatible TSRs (i.e. ones written to be compliant with the
system) to be unloaded in any order. This sounds like a good idea, though I
have not used it. I found ftp://oak.oakland.edu/SimTel/msdos/info/altmpx35.zip
which is dated 13 Sept 1992, but I don't know if this is the latest version.
If someone knows the latest version and its home site, please advise me so I
can include a reference here. (*)
## 6.34 USING INT 8 WITHOUT CHAINING
In some cases, for minimal interrupt overhead when int 8 is being operated at
a high rate, it may be necessary to use int 8 without chaining. Doing this
will cause the DOS time to freeze (unless an RTC-based CLOCK$ driver such as
ATRTC, see section »» 3.3 is installed), will prevent floppy disk drives from
turning off after two seconds of inactivity, will probably prevent timeout-
based 'green' functions (slow-mode, hard drive spindown on laptops, etc) from
kicking in, and will probably break the mouse driver and any network software,
as well as most screen savers and some pop-up TSRs, so this is not something
that should be done by a well-behaved program that is intended for general use.
You can see the effect of disabling the timer interrupt by using the sample
program in section »» 7.12 to set CTC channel zero to an inappropriate mode,
such as mode zero, thus stopping the timer tick.
## 6.35 USING INT 1C HEX INSTEAD OF INT 8
Int 1Ch is intended for use by user programs for timing. It is invoked 18.2065
times per second by the BIOS int 8 handler. On entry to the handler, interrupts
will be disabled. Do not issue an EOI command to the interrupt controller - the
BIOS int 8 handler takes care of this after the int 1Ch handler returns.
TSRs should not use int 1Ch - see below for a discussion of this.
In theory, you should not need to chain to the original int 1Ch handler, as the
default handler is a dummy handler, simply an IRET. However, some existing
TSRs hook int 1Ch. For compatibility with those TSRs you should make your
non-TSR programs chain to the old int 1Ch handler if they use int 1Ch.
See sections »» 6.23 to »» 6.26 for details of what can, and cannot, be safely
done inside an int 1Ch handler.
To use int 1Ch, during initialisation the program should store the address of
the original int 1Ch handler and replace the old handler with a new one, and
on termination, the program should restore the old handler address. Chain to
the old handler in the normal way on every int 1Ch call. It does not matter
whether you chain before you perform your own processing, or after.
A program which intercepts int 8 or int 1Ch should trap critical errors and the
DOS Ctrl-C vector, and optionally the Divide Overflow vector, so that if the
program is terminated due to a critical error or a user Ctrl-Break or Ctrl-C,
the interrupt vector can be restored to its original address as part of the
clean-up. See section »» 5 and subsections for details on trapping the Ctrl-C
and critical error vectors.
In my view, it is inappropriate for a TSR to hook int 1Ch. Some people have
disagreed with this opinion, so for their benefit I will present the evidence
that I have found, and explain the logic by which I arrived at my conclusion.
1 The MS-DOS Encyclopedia has two articles that relate to interrupts and TSRs.
This book is published by Microsoft Press and edited by Ray Duncan. The two
relevant articles are Article 11 on TSRs by Richard Wilton, and Article 13
on Hardware Interrupt Handlers by Jim Kyle and Chip Rabinowitz. Article 11
has a TSR example which uses int 8. The article makes no mention of int 1Ch
at all. Article 13's example code also uses int 8 and the article only
mentions int 1Ch in a table of low-numbered interrupts as "Timer tick (user
defined)".
2 The PC and XT technical references have the following to say about int 1Ch:
"This vector points to the code to be executed on every system-clock
tick. This vector is invoked while responding to the timer interrupt,
and control should be returned through an IRET instruction. The power-
on routines initialise this vector to point to an IRET instruction, so
that nothing will occur unless the application modifies the pointer.
It is the responsibility of the application to save and restore all
registers that will be modified."
3 From the book "DOS Programmer's Reference", 3rd edition, published by Que
Corporation, written by Dettmann, Kyle and Johnson (see section »» 12), in
the section on int 8:
"Int 08h, which is called 18.2 times per second to advance the time-of-
day counter, is tied directly to channel 0 of the system timer chip.
People who write TSRs with utilities such as SideKick, for example,
find Int 08h particularly useful for time-related triggering (as with
a clock or alarm). This interrupt calls Int 1Ch (Timer Tick).
Most TSRs should connect to Int 1Ch rather than to Int 08h.
In the section on int 1Ch:
"Vector 1Ch, the timer tick interrupt called by int 8 (system
metronome), is initialised to point to an IRET instruction. A TSR
that needs to be triggered at each clock tick can reset the vector
for this interrupt to point to a custom interrupt handler.
"Because this function is called from inside the int 08h code, before
handling of that top-priority action is completed, it shares top
priority and will prevent the system from responding to any other
hardware interrupt requests, including those from serial devices
or disk units, while it executes. Therefore it is necessary to keep
to an absolute minimum the time spent in any handler for this function,
or you will risk the loss of data when time-sensitive applications
are running.
"The best practice for a TSR is merely to set a flag from this function,
then inspect the flag from another handler hooked into the int 28h
(DosOK) chain, which gives ample time to take care of any needed
processing without blocking hardware interrupts."
In a section on TSR programming:
"If DOS is not waiting for input, you can use the timer interrupt. The
timer interrupt (1Ch) ticks 18.2 times per second. You can attach to
this interrupt in the following service routine that checks the hot-key
flag as well:
"Timer Interrupt activates
"Call next timer interrupt service
...
The TSRs in the MS-DOS Encyclopedia use int 8 but do not say that int 8 should
be used, and do not give reasons. The DOS Programmer's Reference states clearly
that int 1Ch should be used by TSRs but do not give reasons, and its section on
int 1Ch is worded so as to imply that int 1Ch should not be chained if used in
a TSR, though it is obvious (and clearly shown in the TSR programming section of
the same book) that it should be chained. Since the MS-DOS Encyclopedia is
sanctioned by Microsoft and edited by Ray Duncan, I feel it has more weight
(particularly the hardback edition :-) than the Que book, even though Jim Kyle,
one of the authors of the Que book, co-designed the AMIS TSR interface! The
technical reference also makes the point that the default handler for int 1Ch
is an IRET, and clearly states that "nothing will occur unless the application
modifies the pointer", though this text was written before TSRs were commonplace
and is probably not written with TSR considerations taken into account.
In my view, TSRs should not use int 1Ch, they should use int 8. Applications
may use either (though they must use int 8 if they are speeding up the timer
tick; this is a separate issue). If an application hooks int 8, it must chain
to the original handler. If an application hooks int 1Ch, it should also chain
to the original handler, to support existing TSRs which use int 1Ch.
My logic in coming to this conclusion is:
Int 1Ch is (or was originally) defined for _user_ program use,
The default handler is an IRET, and was provided simply to keep
the machine from crashing when int 1Ch is issued by the
BIOS int 8 handler and no user program is using int 1Ch,
Therefore an application grabbing int 1Ch does not need to chain,
Therefore a TSR writer should not assume that int 1Ch will be chained,
Therefore a TSR writer should use int 8, not int 1Ch.
Some TSRs do use int 1Ch,
Therefore an app using int 1Ch should chain, to support these TSRs.
I believe that a TSR should operate as transparently as possible, i.e. the
environment presented to a user program should be the same with or without the
TSR. The default handler for int 1Ch is an IRET, so an application does not
need to chain when it hooks int 1Ch. If a TSR hooks int 1Ch, the default int
1Ch handler (from an application program's point of view) is no longer an IRET,
and the new 'default' handler must be chained. This has changed the environment
from one where the default handler was just provided so that the machine didn't
crash and there was no reason to chain, to one where chaining is required.
Therefore I regard this as bad programming practice for a TSR writer.
As to the question of whether an application program should chain int 1Ch,
there are clearly some TSRs in existence that do use int 1Ch, so application
programs should now chain int 1Ch. In my opinion this is unfortunate, but due
to the number of programmers who write DOS software, and the lack of thorough
documentation on TSR writing from IBM and Micro$oft, such misunderstandings and
design misfeatures are a sad fact of life. There are other cases where programs
must go to lengths to do things they shouldn't have to do, in order to work
around problems due to bad design in other programs - for example, the old DOS
VDISK program, which grabbed extended memory uncooperatively because it was
written before the XMS standard evolved, is a good example - memory managers
must check for its existence explicitly and refuse to install if VDISK is found.
If you think that this lack of coordination is surprising, consider that an
organised software company developing an operating system would provide its
programming staff with a thorough design document, and allow only experienced
system-level programmers to work on the interrupt routines, whereas Micro$oft
has provided precious little documentation on writing reliable low-level code,
and (because of DOS's lack of support for anything more esoteric than file and
memory management), forced large numbers of programmers with varying amounts of
low-level programming experience to 'go to the hardware' when they want a fast
tick interrupt or a serial port that can operate faster than 300 baud :-) With
this sorry state of affairs, it's a miracle that so many TSRs can live together
at all!
## 6.36 SAMPLE PROGRAM: USING INT 1CH WITH CRITICAL ERROR AND CTRL-C HANDLING
The following program demonstrates using int 1Ch and handling critical errors
and Ctrl-C using the critical error handling module from section »» 5.8.
The program traps Ctrl-C (it has its own Ctrl-C handler) and critical errors
(via the crit_err_intercept() function in CRIT_ERR.ASM), and takes over the
int 1Ch interrupt. It does not chain to the original int 1Ch handler, as this
is supposed to be a dummy IRET instruction. If a badly written TSR is using
this interrupt, then it will just have to miss out while my program is running
(see section »» 6.35 for more details).
Every timer tick, it toggles the speaker state, causing a ticking noise.
The speaker toggle is done inside new_int_1Ch().
First, the user has the opportunity to press Ctrl-C while DOS function 1 is in
progress. This triggers the Ctrl-C handler, which terminates the program with
the message "Program terminated by Ctrl-Break or Ctrl-C". The abort_cleanup()
function is called with dos_is_safe set to TRUE.
If the user presses Enter instead of Ctrl-C, the program continues, and tries
to open the file "A:NOSUCH.FIL". Leave the disk drive empty for this test.
This invokes the critical error handler and issues the Abort, Retry, Ignore
(or Fail) prompt. If the user selects Abort, the critical error intercepter
(in CRIT_ERR.ASM) calls abort_cleanup() with dos_is_safe FALSE, then returns
the Abort error code to DOS, which terminates the program. If the user selects
Fail, the program continues, and calls abort_cleanup() with dos_is_safe TRUE,
then terminates normally via exit(0).
abort_cleanup() resets the control signals for the speaker and cleans up the
int 1Ch vector. If DOS is not safe, it restores the vector directly by patching
the interrupt vector table directly. It only attempts to restore the int 1Ch
vector if the old_int_1Ch variable is not equal to 0xFFFFFFFF, i.e. the vector
has actually been intercepted!
In any case, the ticking sound should stop when the program terminates for any
reason, indicating that the interrupt vectors were correctly restored and the
machine is in a stable state.
-------------------------------- snip snip snip --------------------------------
/*
Sample program #4
Demonstrates using int 1Ch and handling Ctrl-C and critical errors
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save and assemble the critical error module CRIT_ERR (above)
Save this sample code to SAMPLE4.C
Compile this module with:
bcc -c -I<inc_path> -ms sample4.c
Link the modules with:
tlink /c /x <c0_path>\c0s.obj sample4.obj crit_err.obj,
sample4, nul, <lib_path>\cs
Where inc_path is the path to your C header files, c0_path is the path to your
startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB.
*/
#include <dos.h> /* Needed for enable(), disable(), MK_FP() */
#include <fcntl.h> /* Needed for O_RDONLY */
#include <io.h> /* Needed for _open() and _write() */
#include <stdio.h> /* Needed for printf() */
#include <stdlib.h> /* Needed for exit() */
#define FALSE 0
#define TRUE 1
#define STDERR 2 /* DOS handle for standard error */
void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */
unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */
typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */
intfuncp old_int_1Ch = (intfuncp)0xFFFFFFFFL;
void abort_cleanup(int dos_is_safe) {
if (dos_is_safe) {
if (old_int_1Ch != (intfuncp)0xFFFFFFFFL) {
setvect(0x1C, old_int_1Ch);
old_int_1Ch = (void far *)0xFFFFFFFFL;
}
/* Insert other cleanups here - DOS can be safely called */
}
else {
disable(); /* Probably superfluous */
if (old_int_1Ch != (intfuncp)0xFFFFFFFFL) {
*((intfuncp far *)MK_FP(0, 0x1C << 2)) = old_int_1Ch;
old_int_1Ch = (void far *)0xFFFFFFFFL;
}
/* Insert other cleanups here - DOS can NOT safely be called */
}
outportb(0x61, inportb(0x61) & 0xFC); /* Clean up speaker control */
return;
}
void interrupt ctrl_c_handler(void) {
static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n";
if (is_at_crit_prompt())
abort_cleanup(FALSE);
else {
abort_cleanup(TRUE);
_write(STDERR, &message, sizeof(message));
}
exit(255);
}
void interrupt new_int_1Ch(void) {
outportb(0x61, (inportb(0x61) & 0xFE) ^ 0x02);
return; /* From interrupt */
}
void intercept_int_1Ch(void) {
old_int_1Ch = getvect(0x1C);
setvect(0x1C, new_int_1Ch);
return;
}
unsigned int dos_func_1(void) {
_AX = 0x100;
geninterrupt(0x21); /* DOS keyboard input with echo and break */
return _AL;
}
void main(void) {
int n;
printf("Sample program #4 - Demonstrates using int 1Ch and handling Ctrl-C and critical errors\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
crit_err_intercept(); /* Trap critical errors */
setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */
intercept_int_1Ch(); /* Intercept int 1Ch */
printf("Type characters, press Ctrl-C to test the Ctrl-C handler\n");
printf("Press Enter to continue\n");
do {
n = dos_func_1();
} while (n != '\r'); /* Wait for C/R */
printf("Now testing critical error handler, opening 'A:NOSUCH.FIL'\n");
printf("Please remove any disk (if any) from drive A\n");
printf("Select the Abort option to test the critical error handler\n");
n = _open("a:nosuch.fil", O_RDONLY);
if (n != 0 && n != -1)
_close(n);
abort_cleanup(TRUE);
printf("Normal program termination\n");
exit(0);
}
-------------------------------- snip snip snip --------------------------------
## 6.37 DEBUGGING INTERRUPT HANDLERS
Saul Cozens (s.cozens@sheffield.ac.uk) wrote:
> I have noticed that many people attempt to debug interrupt handlers by
> adding a printf statement so they know that the ISR has been called.
Yes, of course printf() is right out. printf() calls DOS (usually), therefore
it cannot safely be used from within an interrupt handler such as an int 8
handler. Saul suggests that the BIOS 'write string' function is a safer bet.
The BIOS video functions are listed as non-reentrant, but often you can get
away with calling them from an interrupt handler. A common technique when
trying to figure out just _what_ an interrupt handler is doing, is to issue
a bell at appropriate points, using int 0x10, function 0x0E with 0x07 in AL.
For a cleaner approach, on every interrupt just clear the Timer 2 Gate bit on
Port B and toggle (flip) the Speaker Enable bit. This will produce a click on
each interrupt. Alternatively, increment a character in screen memory. The
character at offset 0F00h into the regen buffer is the bottom left corner
character in 80x25 text modes.
> I found a bug with Borland C 3.1. When a function is declared as an
> interrupt function, the compiler quite rightly perserves all the registers
> automatically (I used Turbo Debugger to look at the compiled code).
> Unfortunately it does not save the high words of the 32-bit registers, even
> when the options are set to 'use 32-bit registers'.
I have also noticed this problem - in Borland Pascal 7. A library arithmetic
function (long div) uses 32-bit registers, if the appropriate compiler option
is set. If this function is called from within an interrupt routine, the
hiword of EAX is destroyed. The 'interrupt' keyword on the function definition
causes the 16-bit registers to be preserved on the stack, but not the 32-bit
registers. This bug is particularly nasty because this function is called
invisibly to the programmer, as part of an innocent-looking calculation.
There may also be implications when running with a DOS extender.
John Stockton (jrs@dclf.npl.co.uk) sent me the following information which
contains a fix for this problem and also mentions another related problem:
> Duncan Murdoch (dmurdoch@mast.queensu.ca) has provided inline TP/BP code
> to save and restore EAX..EDX in an ISR:
>
> procedure PushEAXtoEDX ; {from DM}
> Inline(
> $66/ {db $66} $50/ {push ax}
> $66/ {db $66} $53/ {push bx}
> $66/ {db $66} $51/ {push cx}
> $66/ {db $66} $52 {push dx} ) ;
>
> procedure PopEDXtoEAX ; {from DM}
> Inline(
> $66/ {db $66} $5A/ {pop dx}
> $66/ {db $66} $59/ {pop cx}
> $66/ {db $66} $5B/ {pop bx}
> $66/ {db $66} $58 {pop ax} ) ;
>
> and he has pointed out that something like:
>
> var X, Y : longint ;
> {...}
> X := 10 ; Y := 10 ;
> { repeatedly : } if X * Y <> 100 then BEEP ;
>
> in the main program can detect this problem and its cure.
>
> Matters are worse if the main program and the ISR both use the hardware
> FPU programmed in Pascal. One can save and restore the FPU state, and
> that does help but does not cure the problem. It seems that TP/BP FPU
> code uses non-reentrant 80x86 routines around 80x87 instructions.
>
> Inspiration dawned during an E-mail exchange with Norbert Juffa
> (norbert@itt.com, whose files should be read by anyone interested
> in Pascal and floating point).
>
> I (with a '486) now compile the ISR in the {$N-,E-} state which forces
> 6-byte software real arithmetic, and the main code in the {$N+,E-} state
> using extended variables and hardware arithmetic. With a little care to
> disable interrupts while transferring values between types real and
> extended, all seems well.
Thank you John for that information.
> Another thing that used to catch me out was that single stepping (using
> Turbo Debugger) through bits of code that re-program the PIT causes a
> system crash. This is presumably because the writes to certain registers
> must be consecutive and the Turbo Debugger writes to the PIT itself every
> time it does a 'step'.
When programming the CTC, the entire access sequence must be completed without
interference, so interrupts must be locked out, and the sequence of accesses
must be executed from start to finish without being interrupted by anything,
including a debug single step interrupt or breakpoint.
This section may be improved later (*)
## 7 HARDWARE INFORMATION AND PROGRAMMING
## 7.1 THE 14.31818 MHZ CLOCK
A crystal oscillator or oscillator module generates a 14.31818 MHz clock which
is divided by 12 to give the 1.1931816666666... MHz clock frequency (period is
12/14318180, or 0.83809534452 us), which is fed to all three channels of the
counter/timer chip. This is the basic timing resolution of the counter/timer.
## 7.2 CLOCK FREQUENCY ACCURACY
The 14.31818 MHz clock's absolute accuracy depends mainly on the quality of the
14.31818 MHz crystal or crystal oscillator module, and is typically in the
region of +/- 5 ppm (0.0005%; 0.4 seconds per day) to +/- 20 ppm (0.002%; 1.73
seconds per day). Errors consist of initial frequency error, and variations
due to temperature and long-term drift. Because of these inaccuracies, there
is little point in specifying times or frequencies to more than five or six
digits as I have done above.
If required, frequency accuracy can be improved by installing a high quality,
close-tolerance crystal, or a high quality crystal oscillator module, which
will reduce all of the above error sources. If accuracy is still inadequate,
with a crystal it may be possible to add a small variable capacitor to the
oscillator circuit, to 'pull' the crystal onto the correct frequency. If
anyone has specific advice on this, please let me know. (*)
Alternatively, your software could incorporate an adjustment so that once the
amount of error has been measured, manually by the user over a long period of
time, it could be corrected by the software. Of course this must be configured
individually for every machine the software will run on, and temperature and
long term drift will still have an effect.
Historical note: If you were wondering "Wouldn't 1 MHz have been easier?", yes
it would, but that would have required an extra crystal. IBM were... er,
'clever' - they used a master clock of 14.31818 MHz, and used logic chips to
derive the 4.77 MHz CPU clock, the timer clock, and the NTSC colour subcarrier
frequency for the CGA card, so they could save a few dollars. Although the
14.31818 MHz signal is not required by modern CPUs and video cards (in fact,
it is now only used for the CTC clock!), the strange frequency still hangs
around like a stale fart - we are stuck with it forever. :-(
## 7.3 THE COUNTER/TIMER CHIP (CTC)
The counter/timer chip (CTC) in the IBM PC family is an Intel 8253 in the PC and
XT, or an Intel 8254 in the AT and later machines (except the PS/2 {JAM}) or a
functional equivalent, and is part of the processor support chipset on the
motherboard. On modern motherboards, it is part of one ASIC in a chipset.
The CTC has three fully independent channels, numbered zero, one, and two.
Each has a clock input, a gate input, and an output, and in the PC, family,
these are wired as follows:
Chan Clock input Gate input Output Channel is used for
---- ----------- ---------- ------ -------------------
0 1.193182 MHz Tied high To IRQ0 Timer tick
1 1.193182 MHz Tied high DRAM refresh DRAM refresh
2 1.193182 MHz Timer 2 Gate Speaker gating Audio generation
Software access to the CTC is via four adjacent addresses in the directly
addressable I/O page. Programming information starts at section »» 7.9.
In most respects the 8253 and 8254 are identical. The following description
applies to both types of CTC unless specifically stated.
## 7.4 CTC CHANNELS
Each channel operates independently, and can be programmed for one of six modes
of operation. Normally, modes 2 or 3 are used. In these modes, the CTC channel
takes the CTC clock (1.193182 MHz) and 'divides' this frequency down to produce
a lower frequency at the output pin. Other modes operate differently.
The frequency division is controlled by the 'divisor' value, a 16-bit unsigned
number between 1 and 65536 (65536 is represented as zero), which is individually
programmable for each channel in the CTC. Setting a very small divisor value
gives a very high output frequency. A divisor of 65536 gives the lowest output
frequency, 18.206507364909 Hz (cycle period is 54.92541649846559 ms).
## 7.4.1 CTC CHANNEL ZERO
CTC channel zero normally operates in mode two or three with a divisor of
65536, giving an output frequency of 18.2065 Hz (period is 54.9254 ms). Its
gate input is tied high. Its output drives the IRQ0 input of the primary PIC
(8259 interrupt controller chip). On every rising edge of the channel zero
output pin (i.e. transition from low to high), IRQ0 is triggered, invoking
interrupt 8, the timer tick interrupt (see section »» 6.1).
## 7.4.2 CTC CHANNEL ZERO DEFAULT OPERATING MODE
Traditionally, CTC channel zero has been set to operate in mode three by the
BIOS POST, but recent 486 BIOSes that I have seen appear to be using mode two
by default. The only significant differences are the width of the pulse from
the CTC pin that triggers the timer tick interrupt, which is narrow in mode
two but is still plenty wide enough for the Intel 8259 PIC chip, and the value
read from the CTC channel zero counter (which decrements twice as quickly in
mode 3).
From a hardware point of view, either mode should work on all motherboards, but
if some code in the BIOS assumes that CTC channel 0 is in the mode that the BIOS
originally programmed, it may not work correctly if CTC channel 0 has been
reprogrammed for the other. Of course, reprogramming the CTC divisor for a
higher sample rate will also cause this problem. The only example of this that
I know of, is the joystick read function (int 15h called with AH = 84h and
DX = 1) (see section »» 10.4.2). Please tell me if you find any other problems
related to changing the mode. (*)
## 7.4.3 CTC CHANNEL ONE
CTC channel one triggers DRAM refresh cycles. DRAM (Dynamic Random Access
Memory) is the main system memory in your computer (typical machines have four
to eight megabytes of RAM, or 32 to 64 megabytes if you want to run Win 95 :-)
DRAM stores data as electrical charges on tiny capacitors inside the chip, and
this type of memory must be refreshed regularly to prevent the capacitors from
discharging. On the PC and XT, refresh cycles are implemented via the DMA
controller. On the AT and later machines, refresh cycles are performed by
dedicated hardware. It appears that the AT does use CTC channel one to
initiate DRAM refreshes, but I have heard that you cannot change the refresh
rate on ATs and later machines. Can anyone shed any light on this? (*)
The normal divisor for CTC channel one is 18, which gives a DRAM refresh cycle
every 15.0857162013608 microseconds. Every refresh cycle forces the processor
to wait briefly, and a popular trick used to be to slow down the DRAM refresh
rate on PCs and XTs by increasing the divisor, to reduce the refresh overhead,
giving a few percent performance improvement, so your flash 8MHz Turbo XT would
actually seem to run at 8.05 MHz! Seems pretty pathetic now, doesn't it :-)
CTC channel one is not even accessible on the PS/2's ASIC {JAM}.
CTC channel one has no interrupt connection, but can be used for timing via the
Refresh Detect signal on bit 4 of Port B. See section »» 7.37.
## 7.4.4 CTC CHANNEL TWO
CTC channel two generates audio for the speaker. It is the most versatile CTC
channel, because its gate input can be controlled by software, and its output
can be read by software via Port B (see section »» 5.5).
CTC channel two can be used for timing, but it cannot generate an interrupt.
See section »» 7.29 and section »» 7.31 for examples of programming CTC channel
two. See the section »» 5.5 for details of the speaker interface.
## 7.5 SPEAKER INTERFACE
The speaker interface on the PC and XT is implemented via the 8255 PPI chip,
which occupies I/O addresses 60h to 62h inclusive, and also provides the
interface to the keyboard. Port B (read/write, at I/O address 61h) and Port
C (read-only, at I/O address 62h) are used by the speaker interface.
On the AT and later machines, which do not have a PPI chip, these functions are
implemented in an ASIC in the chipset, or with discrete logic, as a partly
read-only, partly read/write register at I/O address 61h, known as Port B.
In most respects, the PC/XT and AT interfaces are similar. CTC channel two
gate input can be controlled by software via a read/write bit in an I/O
register; this signal is known as Timer 2 Gate. The CTC channel two output
pin can be read back directly, via a read-only bit in an I/O register, and is
AND-gated with a signal called Speaker Data (software controlled, via a
read/write I/O register bit), the speaker being driven from the output of the
AND gate, sometimes via a simple resistor-capacitor lowpass filter to remove
high frequency components. On the PC and XT only, the speaker control signal
(after the AND gate, and inverted) can also be read back by software, though
this seems to be an undocumented feature and may not work on all machines.
Figure 1 (FIGURES archive) shows the speaker interface signals and circuitry.
PC and XT : I/O address 61h, "PPI Port B", read/write
7 6 5 4 3 2 1 0
* * * * * * . . Not relevant to speaker - do not modify!
. . . . . . * . Speaker Data
. . . . . . . * Timer 2 Gate
PC and XT : I/O address 62h, "PPI Port C", read only
7 6 5 4 3 2 1 0
* * . . * * * * Not relevant to speaker, read-only
. . * . . . . . Timer 2 output read-back
. . . * . . . . Speaker signal (after AND gate, inverted), undocumented
AT and later : I/O address 61h, "Port B", partly read/write, partly read-only
7 6 5 4 3 2 1 0
* * . . . . . . Not relevant to speaker, read-only
. . * . . . . . Timer 2 output read-back, read-only
. . . * . . . . Refresh Detect (read-only), see section »» 7.37
. . . . * * . . Not relevant to speaker - do not modify! (read/write)
. . . . . . * . Speaker Data (read/write)
. . . . . . . * Timer 2 Gate (read/write)
I have a nasty suspicion that the PS/2 may not implement Port B properly. Can
anyone confirm or deny this? (*)
Audio generation can be done via CTC channel two, by setting Timer 2 Gate high
and Speaker Data also high. This enables channel two, and enables its output to
control the speaker directly. Alternatively, if Timer 2 Gate is set low, CTC
channel two output goes high (assuming an appropriate mode is programmed for
channel two), and Speaker Data can be manipulated to drive the speaker directly.
The former technique is used in the sample program in section »» 7.30.
Here is a code fragment that determines whether the speaker hardware is the
PC/XT type or the AT type. It uses bit 7 of the I/O port at 61h. On an XT,
PPI Port B is fully read/write, and bit 7 is the keycode acknowledge signal to
the keyboard interface on the motherboard. On an AT, bits 4-7 of Port B are
read-only, and bit 7 is the motherboard RAM parity error signal. By toggling
bit 7 six times and testing whether the port reads the expected value, this
code fragment determines what type of Port B hardware and keyboard interface
is present. This code destroys AX and CX.
-------------------------------- snip snip snip --------------------------------
pushf ; Keep interrupt flag
mov cx,400h ; Six attempts (top bits of CH)
cli ; Lock out interrupts during this stuff
in al,61h ; Get Port B contents
jmp SHORT $+2 ; Short delay
mov ah,al ; Original value to AH
Flip61Loop: xor ah,10000000b ; Flip top bit
mov al,ah ; Get value to AL
out 61h,al ; Write value to port
jmp SHORT $+2 ; Short delay
jmp SHORT $+2 ; Short delay
in al,61h ; Read it back
xor al,ah ; Set bit 7 if value didn't stay
shl al,1 ; Shift bit into carry
rcl cx,1 ; Shift bit into bottom of CX
jnc Flip61Loop ; Loop if more flips (six in total).
popf ; Restore interrupt flag
test cl,cl ; Was port read/write? Zero if so.
-------------------------------- snip snip snip --------------------------------
This code fragment will leave the zero flag true if the machine is a PC or XT
(i.e. Port B bit 7 is read/write), or zero flag false if the machine is an AT
or later machine (i.e. Port B bit 7 is read-only). You could follow it with
the instruction:
jnz Not_PCXT ; If not, it's an AT
## 7.6 CTC INTERNAL REGISTERS
Each CTC channel operates independently. Each channel contains:
■ A 6-bit Mode register
■ A 16-bit Reload register (the 'divisor register' in modes 2 and 3)
■ A 16-bit Counting register (the 'Counting Element' in Intel docs)
■ A 16-bit Latch register
■ An 8-bit Status Latch register
■ A lobyte/hibyte flag
■ A 'T' (toggle) flip-flop, used in mode three
The major functional blocks are shown in Figure 3 (in the FIGURES archive).
The Mode register controls the operating mode (section »» 7.8) and the access
mode (see section »» 7.7) of the channel. It is written at the start of the
programming sequence. When it is written, the channel output pin usually goes
into a defined state - see the individual mode descriptions, section »» 7.8.
The Reload register can be programmed by software. The Counting register is
reloaded from this register at certain times (depending on the operating mode).
In modes 2 and 3, which operate as frequency dividers, this register is also
called the divisor register.
The Counting register is a down-counting 16-bit counter. Its exact behaviour
depends on the operating mode, but generally it counts down on every CTC
clock pulse (0.8381 us). It cannot be read directly - it is always read via
the Latch register.
The Latch register is a 16-bit software-readable transparent latch which follows
the Counting register unless the Latch command is issued. This command makes
the Latch register freeze, so that a stable count value can be read.
The 8-bit Status Latch register is used with the read-back function when the
channel status is latched, see section »» 7.18.
The lobyte/hibyte flag is an internal flag which determines which half of the
16-bit Reload and Latch registers will be accessed through the 8-bit access
port.
## 7.7 ACCESS MODES
Because the I/O interface to the CTC is only eight bits wide, the CTC implements
three Access modes which control how values are written to the Reload registers
and read from the Latch registers.
■ Lobyte only
■ Hibyte only
■ Lobyte then hibyte (using the lobyte/hibyte flag)
If lobyte only, or hibyte only, are selected, the registers are read or written
with a single access. If lobyte/hibyte access is selected, a read or write to
the data port will access the lobyte or hibyte of the registers, according to
the lobyte/hibyte flag, which toggles automatically after each register access.
In the lobyte/hibyte access mode, two 8-bit accesses are required to fully read
the Latch register and to fully write the Reload register. Regardless of the
access mode, the Counting register always operates as a 16-bit counter.
If a channel is set for lobyte-only or hibyte-only access, when the data port
is written, the other byte is taken to be zero. For example, for a channel set
for lobyte-only access, writing 50 to the data port will set the reload register
to 50, and a write of zero to the data port will set the reload register to 0,
i.e. a divisor of 65536 in modes 2 and 3. For a channel set for hibyte-only
access, a write of 50 to the data port will load the reload register with 12800.
## 7.8 CTC OPERATING MODES
Each channel in the CTC can be independently set to one of six operating modes:
■ Mode 0: Interrupt on terminal count
■ Mode 1: Hardware-retriggerable one-shot
■ Mode 2: Rate generator
■ Mode 3: Square wave generator
■ Mode 4: Software-triggered strobe
■ Mode 5: Hardware-triggered strobe
While reading the mode descriptions below, you may want to refer to section
»» 7.3 and »» 7.4 for the gate and output connections for each channel.
## 7.8.1 OPERATING MODES: BEHAVIOUR COMMON TO ALL MODES
When the mode word is written, all internal logic in the channel, including the
lobyte/hibyte flag, is reset, and the output immediately goes to the initial
state (which depends on the mode).
A new value can be written into the Reload register at any time. The operating
mode determines the exact effect that this will have, see the individual mode
descriptions below.
Loading and decrementing of the Counting register occurs on the _falling_ edge
of the CTC clock input.
The CTC samples the gate input on the _rising_ edge of the CTC clock input.
In modes one, two, three, and five, a rising edge on the gate input sets an
internal flip-flop, whose output is sampled on rising edges of CTC clock.
This flip-flop is reset after its output has been sampled. Therefore the
timing of the rising edge on gate need not be synchronised with CTC clock.
In modes where falling edge on CTC clock loads the Counting register and also
decrements it, the Counting register is not decremented on the CTC clock pulse
which loads it. It starts decrementing on the _next_ CTC clock.
The BCD/Binary flag allows BCD operation to be selected. In BCD mode, the
Counting register operates in 4-digit binary-coded-decimal format. If the
Counting register is zero and is decremented, it wraps around to 9999 hex.
The Intel documentation does not describe how the chip will behave if the
Reload register contains any digits outside the range 0-9 and I have not
tested to find this out, as it may be implementation dependent. Also this
feature is not normally used in the PC, and may well be non-functional on
some workalikes (chipsets). In other words, don't use BCD mode!
## 7.8.2 OPERATING MODE ZERO: INTERRUPT ON TERMINAL COUNT
When the mode word is written, the output pin goes low and the CTC waits for
the Reload register to be loaded by software, whereupon it transfers the value
in the Reload register into the Count register on the next falling edge of the
CTC clock. Subsequent falling edges of CTC clock will decrement the Counting
register _if the gate input is high_. If the gate is low, the Counting register
will not decrement. The gate input is sampled on the rising edge of CTC clock.
When the Counting register decrements from one to zero, the output goes high,
and remains high until another Mode word is written, or another value is written
into the Reload register. The Counting register continues to count even after
it has decremented to zero - it wraps around to FFFF hex (9999 in BCD mode) -
but this doesn't affect the output pin state.
The Reload register may be written at any time. In two-byte access mode, when
the first byte of the Reload register is written, counting stops and the output
goes low. Once the Reload register is loaded, the next clock pulse will load
the Counting register from the Reload register, and counting will resume,
starting from the new value.
See section »» 7.31 for an example of this mode being used with channel two
and section »» 10.7 for this mode being used in PWM audio generation.
## 7.8.3 OPERATING MODE ONE: HARDWARE-RETRIGGERABLE ONE-SHOT
This mode uses the gate input as a trigger. Gate is sampled on the rising edge
of CTC clock. The trigger occurs on the rising edge of the gate input.
When the mode word is written, the output pin goes high and the CTC waits for
the Reload register to be loaded by software. It is then armed, and waits for
a rising edge on the Gate input. Once this is detected, the next falling edge
of CTC clock sets the output low and transfers the Reload register into the
Counting register, and counting is enabled. On every subsequent falling edge of
CTC clock, the Counting register decrements. When the Counting register
decrements from one to zero, the output returns high and remains high, though
the Counting register continues to decrement (it wraps around).
During the counting period, the gate input may go low, and this will be ignored.
A rising edge on gate during counting (a re-trigger) will cause the Reload
register to be transferred into the Counting register on the next falling edge
of CTC clock, as above, thus restarting the timer and extending the low-pulse
at the output.
The Reload register may be written at any time, but this will not affect the
count in progress. This will affect the value reloaded into the Counting
register when re-triggered.
This mode is not used with channel 0 or 1, as their gate inputs are tied high.
## 7.8.4 OPERATING MODE TWO: RATE GENERATOR
In mode two, the channel operates as a frequency divider. The reload register
becomes the divisor, by which the CTC clock frequency is divided, to produce
the output frequency. A low gate input stops the counter. When gate returns
high, the counting register is reloaded and the count sequence begins again.
When the mode word is written, the output goes high. When the Reload register
has been written, the Reload register is transferred to the Counting register
on the next falling edge of the CTC clock. The Counting register decrements by
one on every falling edge of CTC clock.
When the Counting register is decremented to one, the channel's output goes low.
On the next falling edge of CTC clock the Counting register is reloaded from
the Reload register, the output returns high, and the cycle continues.
If the gate input goes low, counting stops and the output goes high immediately.
Once the gate input has returned high, the next falling edge on CTC clock
reloads the Counting register from the Reload register and operation continues.
Programming a new value into the Reload register does not affect the count in
progress. The next reload (due to the Counting register reaching 1 or due to
the gate input going low then high) starts from the newly programmed value.
A divisor (Reload register) value of one must _not_ be used with this mode.
To summarise, the Counting register starts at the Reload register value and
decrements down to one, then reloads. The output is low while the Counting
register is equal to one. Thus output pulses are generated at 1.193182 MHz
divided by the Reload register (divisor) value. The period between output
pulses is the CTC clock period (0.8381 us) multiplied by the Reload register
(divisor) value, and they are one CTC clock period wide.
This makes mode two unsuitable for use with timer two for generating audio for
the speaker, because the speaker cannot respond to such short pulses. For this
reason, the 8254/8253 has operating mode three.
## 7.8.5 OPERATING MODE THREE: SQUARE WAVE GENERATOR
Like mode two, mode three operates as a frequency divider. The difference is
in the output signal. Whereas mode two produces a short pulse for every timer
reload, mode three produces a square wave output.
In this mode, the reload pulse is fed into an internal 'T' (toggle) flip-flop,
which toggles (reverses state) on each pulse, and the output of this flip-flop
becomes the output signal. Every time the Counting register reloads, the output
pin toggles to the opposite state. This gives a square wave output, with equal
high and low times (i.e. a 50% duty cycle, or 1:1 mark to space ratio). If an
odd divisor is used, the duty cycle is not exactly 50% (as explained below).
However, two reloads are needed to produce one output cycle, so the reload rate
must be doubled to compensate for the halving action of the 'T' flip-flop. This
is accomplished by making the Counting register decrement by two instead of by
one for every CTC clock. So in mode three, the Counting register decrements in
steps of two and reloads twice as fast as it would in mode two, and the twice-
speed reload frequency is halved by the 'T' flip-flop to produce an even square
wave output at the correct frequency.
Odd divisor values are handled strangely. On every reload, the Reload register
minus one (which will be an even value) is loaded into the Counting register.
If the output pin is high, the chip waits until the Counting register has
decremented to zero (not one, as would be normal), and reloads the Counting
register on the next CTC clock after that. If the output pin is low, it reloads
the Counting register after the Counting register reaches one, as normal. This
makes the high pulse one CTC clock cycle wider than the low pulse, and shifts
the output square wave's duty cycle slightly above 50%. The duty cycle error
is only significant if the divisor value is small.
The output pin goes high immediately when the mode word is written. Once the
Reload register has been written, counting begins.
If the gate input drops low, counting stops and the output pin goes high
immediately. When the gate input has returned high, the next falling edge
on CTC clock reloads the Counting register from the Reload register, leaving
the output pin high, and counting resumes. If the Reload register is written
while counting is in progress, the new value has no effect until a reload
occurs, either due to the gate input going low then high, or due to a normal
reload, which happens twice for every output cycle.
A divisor (Reload register) value of one must _not_ be used with this mode.
As well as the different output generated by the timer in modes two and three,
there is a difference when the timer is read on-the-fly - see section »» 9.
## 7.8.6 OPERATING MODE FOUR: SOFTWARE-TRIGGERED STROBE
Mode four operates as a retriggerable delay, generating a pulse when the delay
expires. When the mode word is written, the output pin goes high. Once the
Reload register has been written, the next falling edge of CTC clock loads the
Counting register from the Reload register, and counting begins. When the
Counting register decrements to zero, the output goes low for one CTC clock
pulse then returns high. The Counting register continues to decrement,
wrapping round to FFFF hex (or 9999 hex in BCD mode), but no more output pulses
will occur.
If the Reload register is written during counting, after the Reload register is
fully written (both bytes, if programmed for lobyte/hibyte access), the next
falling edge of CTC clock reloads the Counting register, retriggering the delay
period or starting a new delay if the previous delay had expired.
A low gate input disables counting but the gate input has no other effect.
## 7.8.7 OPERATING MODE FIVE: HARDWARE-TRIGGERED STROBE
Mode five is a cross between mode one and mode four, using a rising edge on the
gate input to trigger or retrigger the delay period.
When the mode word is written, the output pin goes high and the CTC waits for
the Reload register to be loaded by software. It is then armed, and waits for
a rising edge on the Gate input. Once this is detected, the next falling edge
of CTC clock transfers the Reload register into the Counting register, and
counting is enabled. On every subsequent falling edge of CTC clock, the
Counting register decrements. When the Counting register decrements to zero,
the output goes low for one CTC clock pulse width then returns high. The
Counting register continues to decrement, wrapping round to FFFF hex (or 9999
hex in BCD mode), but no more output pulses will occur until the channel is
re-triggered by another rising edge on the gate input.
During the counting period, the gate input may go low, and this will be ignored.
A rising edge on gate during counting (a re-trigger) will cause the Reload
register to be transferred into the Counting register on the next falling edge
of CTC clock, as above, thus restarting the timer and re-triggering the delay.
The Reload register may be written at any time, but this will not affect the
count in progress. This will affect the value reloaded into the Counting
register when re-triggered.
This mode is not used with channel 0 or 1, as their gate inputs are tied high.
## 7.9 THE 8254/8253 REGISTERS
On the PC family, the 8254/8253 timer occupies four I/O addresses in the
directly addressable I/O page, as follows:
40h Channel 0 data port (read/write)
41h Channel 1 data port (read/write)
42h Channel 2 data port (read/write)
43h Mode/Command register (write only - read is ignored)
## 7.9.1 THE MODE/COMMAND REGISTER
The Mode/Command register at I/O address 43h is defined as follows:
7 6 5 4 3 2 1 0
* * . . . . . . Select channel: 0 0 = Channel 0
0 1 = Channel 1
1 0 = Channel 2
1 1 = Read-back command (8254 only)
(Illegal on 8253)
(Illegal on PS/2 {JAM})
. . * * . . . . Command/Access mode: 0 0 = Latch count value command
0 1 = Access mode: lobyte only
1 0 = Access mode: hibyte only
1 1 = Access mode: lobyte/hibyte
. . . . * * * . Operating mode: 0 0 0 = Mode 0, 0 0 1 = Mode 1,
0 1 0 = Mode 2, 0 1 1 = Mode 3,
1 0 0 = Mode 4, 1 0 1 = Mode 5,
1 1 0 = Mode 2, 1 1 1 = Mode 3
. . . . . . . * BCD/Binary mode: 0 = 16-bit binary, 1 = four-digit BCD
You might prefer the following diagram and explanation.
7 6 5 4 3 2 1 0
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ SC1 │ SC0 │ RL1 │ RL0 │ M2 │ M1 │ M0 │ BCD │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
│ │ │ │ │ │ │ │
COMMAND SELECT BITS MODE SPECIFIER BITS
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ ├─ Binary/BCD mode
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ 0 = Binary
│ │ │ │ │ │ │ 1 = BCD
│ │ │ │ │ │ │
│ │ │ │ ├─────┼─────┼── Mode number
│ │ │ │ │ │ │
│ │ │ │ 0 0 0 = Mode 0
│ │ │ │ 0 0 1 = Mode 1
│ │ │ │ 0 1 0 = Mode 2
│ │ │ │ 0 1 1 = Mode 3
│ │ │ │ 1 0 0 = Mode 4
│ │ │ │ 1 0 1 = Mode 5
│ │ │ │ 1 1 0 = Mode 2
│ │ │ │ 1 1 1 = Mode 3
│ │ │ │
│ │ ├─────┼── Latch/Read/Write operation
│ │ │ │
│ │ 0 0 = Latch count value command (for read)
│ │ 0 1 = Read/Write lobyte only
│ │ 1 0 = Read/Write hibyte only
│ │ 1 1 = Read/Write lobyte then hibyte
│ │
├─────┼── Timer/counter number
│ │
0 0 = Select channel 0
0 1 = Select channel 1
1 0 = Select channel 2
1 1 = Read-back command on 8254 (not allowed on 8253 and PS/2)
The SC1 and SC0 (Select Channel) bits form a two-bit binary code which tells
the CTC which of the three channels (channels 0, 1, and 2) you are talking to,
or specifies the read-back command. As there are no 'overall' or 'master'
operations or configurations, every write access to the mode/command register,
except for the read-back command (see section »» 7.18), applies to one of the
channels. These bits must always be valid on every write of the mode/command
register, regardless of the other bits or the type of operation being performed.
The RL1 and RL0 bits (Read/write/Latch) form a two-bit code which tells the CTC
what access mode you wish to use for the selected channel, and also specify the
Counter Latch command to the CTC. For the Read-back command, these bits have a
special meaning (section »» 7.18). These bits also must be valid on every write
access to the mode/command register.
The M2, M1, and M0 (Mode) bits are a three-bit code which tells the selected
channel what mode to operate in (except when the command is a Counter Latch
command, i.e. RL1,0 = 0,0, where they are ignored, or when the command is a
Read-back command, where they have special meanings, see section »» 7.18).
The modes are described in section »» 7.8 and subsections. These bits must
be valid on all mode selection commands (all writes to the mode/command
register except when RL1,RL0 = 0,0 or when SC1,0 = 1,1).
Like the Mode specification, the BCD bit must be valid on all mode selection
commands. This bit simply specifies whether the channel will count in binary
(the usual mode) or BCD, when it will behave as four separate cascaded 4-bit
BCD counters. The counters always count DOWNWARDS, which can make BCD mode
awkward to use. Also see section »» 7.8.1.
## 7.9.2 THE DATA PORTS
Writing to the data ports sets the Reload register (one or two writes are used,
according to the access mode - see section »» 7.7). Reading the ports returns
the Latch register (lobyte, hibyte, or alternating lobyte and hibyte, depending
on the access mode, see section »» 7.7) or the status register if a status
read-back command has just been issued (see section »» 7.18).
## 7.9.3 ACCESSING THE REGISTERS
Accessing a CTC channel involves writing one byte to the mode/command register
at I/O address 43h, to tell the chip what you want to do, followed by reading or
writing one, two or sometimes three bytes in succession, to or from the data
port for the appropriate channel. This should always be done with interrupts
disabled, because the CTC "remembers where it's up to", and will get confused
if the normal sequence of register accesses is interrupted.
Always use byte-sized I/O instructions to access these ports. In assembly, use
OUT nn,AL or IN AL,nn (not AX). In C, use inportb() and outportb() or the
equivalent 8-bit I/O functions or pseudofunctions for your compiler.
## 7.9.4 I/O RECOVERY DELAYS
Modern CPUs operate internally and externally at very high speeds. Modern fast
machines must be compatible with old ISA bus cards, which have slow peripheral
devices such as serial ports, parallel ports, video and disk controllers, etc,
accessed via the CPU's I/O space. I/O-addressed peripherals on the motherboard
(the 8254/8253 CTC, the 8237 DMA controllers, the 8259 interrupt controllers,
the real time clock, etc) are also slow by the standards of a modern CPU.
On these fast machines, whenever the CPU makes an access to an I/O device (via
the IN and OUT instructions and variants), hardware on the motherboard must
slow down the access, in order to guarantee that the timing requirements of the
slow peripheral are not violated (i.e. to give the peripheral enough time to
provide or accept the data correctly and prepare for the next data transfer).
There are two parameters of interest - the access time, and the recovery time.
These times are in the order of several hundred nanoseconds, but depend on the
motherboard and peripheral device in question. These times do not apply to
memory accesses, which are cached and are much faster and use few wait states.
The following diagram is a _simplified_ representation of what happens when the
CPU executes two I/O read instructions (for example, "in al,42h / in al,40h").
┌ Valid ┌─42h────────────┐ ┌─40h────────────┐
ADDRESS │ │ │ │ │
└ Not valid ───┘ └────────┘ └──...
: : : :
┌ Valid ┌─IOR────────────┐ ┌─IOR────────────┐
CONTROL │ │ │ │ │
└ Not valid ───┘ └────────┘ └───...
├───Tacc───┤ ├──Trec──┼───Tacc───┤ :
┌ Valid : ┌─────┐ : ┌─────┐
DATA │ : │ │ : │ │
└ Not valid ──────────────┘ └───────────────────┘ └────...
: : : : : : : :
NOTES time--> a b c d e f g h
At point 'a' the CPU makes an I/O read request. The address and control buses
become valid. The address decoding logic sees that the address is in the range
40h-43h and selects the CTC. From this point, the CTC takes Tacc (the access
time) to get itself ready and present the data on the data bus. At point 'b'
the CTC has made the data available on the data bus. At 'c' the CPU reads the
data from the data bus. At point 'd' the cycle is complete and the address and
control buses go inactive. The CPU transfers the data into the AL register.
At point 'e' the CPU generates a second access cycle just like the first. The
peripheral also requires a certain amount of time to elapse between points 'd'
and 'e' - this is called the peripheral's recovery time.
The access time is the time required by the peripheral to accept data correctly
(for an I/O write) or provide data correctly (for an I/O read). It is required
on every access to an I/O device. The recovery time is the time required by
the peripheral _after_ an I/O access, before the peripheral is ready to receive
another I/O request.
An analogy would be a little old lady in a car at an intersection. When the
light changes, she fumbles around looking for the handbrake, then she tries to
remember which pedal to push to go faster. Then she finally takes off. This
is like the access time requirement. Then at the next intersection, she has to
slow down and stop, and get ready for the lights to change again. This takes
time. If the lights change before she has stopped and finished getting ready,
she will do something stupid like crunching the gearbox or driving into a tree.
This requirement is the recovery time.
On slow motherboards (the old PC and XT, and probably most 286-based boards),
the access time and recovery time are both guaranteed to be met because the
CPU's bus interface is fairly slow, and comparatively fast peripheral devices
are used (the 8254's recovery time is 165 or 200 ns, compared to the old 8253's
recovery time of 1000 ns!).
On fast motherboards, the access time is assured because the chipset on the
motherboard inserts I/O wait states, but on some fast motherboards, notably
some 286 and early 386 motherboards, the _recovery_ time is not guaranteed.
The motherboard says "I have to wait until this peripheral is ready, but after
the access is complete, I don't care". With these boards, two back-to-back I/O
accesses to the _same_ peripheral (such as the sequence shown in the diagram
above) will cause the second access to be ignored or misinterpreted by the
peripheral.
This design misfeature of some 286 and 386 motherboards is probably the result
of a design compromise. From the point of view of a chipset designer there are
several ways to deal with I/O recovery time requirements -
1. Enforce a recovery time after every I/O access, or
2. Enforce a recovery time between any back-to-back I/O accesses, or
3. Enforce a recovery time between back-to-back I/O accesses to the
_same_ peripheral, or
4. Never enforce a recovery time after an I/O access.
The first alternative would slow the machine unduly, because the recovery time
would be enforced even if the next accesses were memory accesses (which would
not affect the I/O-addressed peripheral). The second option is complicated to
implement (though modern motherboards use this method, I believe). The third
option is even more complicated. The fourth is the simplest approach, but it
means that back-to-back accesses to the same peripheral will violate that
peripheral's recovery time requirements.
To support these motherboards, programmers would insert the famous "jmp short
$+2" sequence into their code between back-to-back I/O accesses to the same
device. The instruction is effectively a NOP (no-operation) instruction but
it has an extra delaying effect because it clears the processor's instruction
prefetch queue on the 286 and 386, requiring an external bus access, which must
wait for the I/O access cycle to complete.
Modern motherboards detect back-to-back I/O accesses, and insert wait states to
ensure that recovery times are not violated, so there is no need to use this
trick with them, but to support older systems, you may wish to do so. The
sample code and programs in this document do use the "jmp short $+2" trick,
because I am in the habit of using it.
In C, you could use the inline assembler feature of most compilers, but Michael
Mauch (mauch@uni-duisburg.de) advises that the optimiser may optimise out this
instruction, so he suggests (for Borland C++ 3.1 and 4.0), __emit__(0xEB,0x00);
which is not optimised out. You could set up a macro, e.g. #define breather
__emit__(0xEB,0x00). In Turbo Pascal, you could use the appropriate directive
to emit the two-byte instruction. The object code is $EB/$00. If anyone knows
a better or more generic way to implement this in C and/or Pascal, please tell
me. (*)
An alternative method to the "jmp short $+2" method is to insert an access to
another I/O location. This enforces another access time delay, which should
cover the recovery time requirements of the first device. You could then
interleave accesses to the device you are interested in, with accesses to the
'dummy' device. Apparently it is common practice to use "in al,61h" as the
dummy instruction for this purpose. Port 61h is Port B (see section »» 7.5)
and it can be read at any time with no unusual side-effects, so is ideal for
this purpose, except that the IN instruction destroys AL, which is often
inconvenient. An OUT instruction is more covenient but there is no port that
can safely have any value OUTed to it. In the quoted message below, Bob Smith
(bobs@access.digex.net) mentions that some IBM BIOSes use an OUT to port 4Fh
(an unused I/O address) to insert delays.
In an article in Usenet newsgroup comp.lang.asm.x86 in December 1995, Bob Smith
(bobs@access.digex.net) posted the following interesting information:
> The reason there is a short jump to the next instruction is certainly, as
> [most people would say], that some I/O devices need more recovery time.
> Moreover, the 386 processor treats a flush of the prefetch queue specially
> with respect to I/O operations. The problem is that that trick doesn't work
> any more! Quoting from the (now out-of-print) "i486 Microprocessor Data
> Sheet" (Intel order #240440-001):
>
> "6.3.1 WRITE BUFFERS AND I/O CYCLES
>
> "Input/Output (I/O) cycles must be handled in a different manner by the write
> buffers. I/O reads are never reordered in front of buffered memory writes.
> This ensures that the 486 microprocessor will update all memory locations
> before reading status from an I/O device.
>
> "The 486 microprocessor never buffers single I/O writes. When processing an
> OUT instruction, internal execution stops until the I/O write actually
> completes on the external bus. This allows time for the external system to
> drive an invalidate into the 486 microprocessor or to mask interrupts before
> the processor progresses to the instruction following OUT. Repeated OUT
> instructions will be buffered.
>
> "I/O device recovery time must be handled slightly differently by the 486
> microprocessor than with the 386 microprocessor. I/O device back-to-back
> write recovery times could be guaranteed by the 386 microprocessor by
> inserting a jump to the next instruction in the code that writes to the
> device. The jump forces the 386 microprocessor to generate a prefetch bus
> cycle which can't begin until the I/O write completes.
>
> "Inserting a jump to the next write will not work with the 486 microprocessor
> because the prefetch could be satisfied by the on-chip cache. A read cycle
> must be explicitly generated to a non-cacheable location in memory to
> guarantee that a read bus cycle is performed. This read will not be allowed
> to proceed to the bus until after the I/O write has completed because I/O
> writes are not buffered. The I/O device will have time to recover to accept
> another write during the read cycle."
>
> FWIW, I have seen some BIOSes (in IBM systems) use an OUT (of any value) to
> I/O port 4Fh (an otherwise unused port) in order to provide the needed
> synchronization.
Thanks Bob for that information. I believe Glen Blankenship (obother@netcom.
com) also quoted the same information in a separate message.
## 7.10 PROGRAMMING THE MODE AND RELOAD REGISTER
Until initialised, all channels are in an undefined state. The BIOS POST sets
the operating modes for all channels. Channels can be programmed in any order.
For any particular channel, the Mode register must be programmed first. Once
the mode is set, one or two bytes (depending on the access mode - see section
»» 7.7) are written into the data port for that channel; these are loaded into
the Reload register. The channel is then initialised and begins operating
according to the programmed mode.
To program the mode and reload value for a CTC channel, issue a command byte of
ccaammmb binary, where 'cc' = channel number, 'aa' = access mode, 'mmm' = mode,
'b' = BCD/binary selection (see section »» 7.9.1 for the bit definitions) then
write the lobyte, or the hibyte, or lobyte then hibyte (depending on the access
mode) of the reload value to the data port of the selected channel.
Note - a reload value of 1 should NOT be used in modes two and three. Also,
in these modes, low reload values will give very high output frequencies, and
are not normally used with channel zero because the tick rate would be too high.
The Reload register may be reprogrammed at any time, just by writing the lobyte,
hibyte, or lobyte then hibyte (depending on the access mode), to the data port.
See section »» 7.7 and subsections for details.
## 7.11 EFFECT OF REPROGRAMMING CHANNEL ZERO ON THE TIMER TICK INTERRUPT
The system time is maintained using the timer tick interrupt, and reprogramming
the mode of channel zero will reset the channel. When the mode word is written,
the channel zero output pin of the CTC goes high immediately. If it was already
high, no interrupt is generated. If it was low, an interrupt _is_ generated,
causing the BIOS timer tick count variable to be incremented incorrectly. If
CTC channel zero was previously programmed for mode 2, its output would already
be high, and no extra interrupt would be generated.
This should not be done continuously in an application unless you restore the
correct DOS time at termination. Normally it is sufficient to reprogram channel
zero at the start of your program, and leave it in that mode until finished.
This does cause a slight jump in the time, but as it only happens once on every
run of the program, it is not really worth worrying about.
If you want to be more careful, you can wait until the timer tick interrupt
occurs, and reprogram channel zero immediately after the interrupt has occurred.
You can detect the interrupt by watching the BIOS tick count variable until it
changes (only the loword need be monitored, as it always changes on each tick
interrupt). When the interrupt has just occurred, the channel zero output pin
will be high, so reprogramming the channel will not generate an interrupt, and
the Counting register will be near the start of its 54.9254 ms cycle.
A similar approach should be used when terminating the program, after channel
zero has been reprogrammed with a smaller divisor to give a faster tick rate.
Assuming your int 8 handler chains to the BIOS int 8 handler every 54.9254 ms,
wait until the tick count changes (i.e. your int 8 handler has called the BIOS
int 8 handler), then reprogram channel zero with the default parameters (mode
3 or 2, divisor of 65536).
These considerations do not apply when the Reload register is loaded without a
mode initialisation command written to the Mode/Command register, as done with
the dynamic interrupt rate technique (see section »» 8.6).
## 7.12 SAMPLE PROGRAM: PROGRAMMING THE MODE AND RELOAD VALUE
This function programs the operating mode and the reload value (the divisor in
modes two and three) for a specified channel. If you use channel zero in a
non-standard setup, you should restore it to its normal mode and divisor (mode
two or three, with a divisor of 65536) when you've finished using it. See
section »» 5 for details of how to intercept the Ctrl-C and Critical Error
vectors, so that you can restore the normal mode at program termination even
if the program is terminated by Ctrl-Break or Ctrl-C being pressed by the user,
or due to a critical error.
The init_channel() function accepts a channel number, a reload value which will
be in the range 0 to 65535 (in modes two and three, a zero divisor gives
division by 65536, and a divisor of one should not be used), and an operating
mode number. The access mode is not provided as a parameter - the function
always programs the channel for lobyte/hibyte access.
See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-------------------------------- snip snip snip --------------------------------
/*
Sample program #5
Program the operating mode and reload value for a CTC channel
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save this file to SAMPLE5.C and compile with:
bcc -I<inc_path> -L<lib_path> -ms sample5.c
Where inc_path is the path to your C header files and your startup modules
C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, and cli */
#include <ctype.h> /* Needed for toupper() */
#include <process.h>
#include <stdio.h> /* Pass go, add printf(), program is 8K already :-) */
#include <stdlib.h> /* Needed for atoi() */
char *accessmodes[] = {
"",
"lobyte-only",
"hibyte-only",
"lobyte/hibyte"
};
void init_channel(unsigned int channum, unsigned int accessmode,
unsigned int mode, unsigned int reload) {
if (channum > 2 || accessmode < 1 || accessmode > 3)
return;
asm pushf; /* Preserve interrupt flag */
asm cli;
outportb(0x43, (channum << 6) + (accessmode << 4) + ((mode & 0x07) << 1)); /* Mode */
if (accessmode & 1)
outportb(0x40 + channum, reload & 0xFF); /* Reload reg lobyte */
if (accessmode & 2)
outportb(0x40 + channum, (reload >> 8) & 0xFF); /* Reload reg hibyte */
asm popf; /* Restore interrupt flag */
return;
}
void usage(void) {
printf("Usage: SAMPLE5 <channel> <accessmode> <operatingmode> <reload>\n\n");
printf("\tchannel is 0, 1, or 2\n");
printf("\taccessmode may be:\n");
printf("\t\tL = Lobyte only\n");
printf("\t\tH = Hibyte only\n");
printf("\t\tW = Lobyte/hibyte (16-bit)\n");
printf("\toperatingmode may be:\n");
printf("\t\t0 = Interrupt on terminal count\n");
printf("\t\t1 = Hardware-retriggerable one-shot\n");
printf("\t\t2 = Rate generator\n");
printf("\t\t3 = Square wave generator\n");
printf("\t\t4 = Software-triggered strobe\n");
printf("\t\t5 = Hardware-triggered strobe\n");
printf("\treload is an unsigned 16-bit value, use zero for divide-by-65536\n");
return;
}
void main(unsigned int argc, char * argv[]) {
unsigned int channum, accessmode, mode, reload;
printf("Sample program #5 - Set the mode and reload value for a CTC channel\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
if (argc < 5) {
usage();
exit(1);
}
channum = argv[1][0] - '0';
switch (toupper(argv[2][0])) {
case 'L':
accessmode = 1;
break;
case 'H':
accessmode = 2;
break;
case 'W':
accessmode = 3;
break;
default:
usage();
exit(1);
}
mode = argv[3][0] - '0';
if (channum > 2 || mode > 5) {
usage();
exit(1);
}
reload = atoi(argv[4]);
printf("Setting CTC channel %d for %s access, mode %d, with " \
"reload value %ld\n", channum, accessmodes[accessmode],
mode, (long)(reload ? reload : 65536L));
init_channel(channum, accessmode, mode, reload);
exit(0);
}
-------------------------------- snip snip snip --------------------------------
## 7.13 READING THE RELOAD REGISTER
It is not possible to read the Reload register contents. In modes two and
three, it may be possible to infer the reload register value using clever
techniques, but I don't believe there is any good reason to pursue this.
## 7.14 READING THE COUNTING REGISTER
Reading the Counting register on-the-fly gives you a fairly accurate time value
with a resolution of 0.8381 us for calculating elapsed time or timestamping
internal or external events. You do not actually read the Counting register
directly, it is read via the Latch register, which follows the Counting register
value unless it is latched via the latch command.
You can read the Counting register by making one or two (depending on the access
mode) reads from the data port of the appropriate channel, however this value is
not latched, and is not stable. In lobyte/hibyte access mode, there is a delay
between reading the lobyte and hibyte, so the lobyte and hibyte don't correspond
to the same instant in time, and you may read an incorrect value. This problem
does not occur if the access mode is lobyte-only, or hibyte-only.
{JAM} Some CTC hardware implementations do not buffer the counter properly, so
if the Counting register is read at the instant it is changing value, you may
read the counter part-way through the 'ripple-through', i.e. some low-order bits
may have decremented but high-order bits may not have decremented yet.
Therefore, even in lobyte-only or hibyte-only mode, the Counting register cannot
be read reliably in this way.
The CTC provides a latch command to avoid these problems. When the latch
command is issued, the Latch register freezes, and the Counting register
continues to count. Thus the Latch register contains a stable count which
can be read via the data ports in the normal way. Once the appropriate number
of bytes (one or two, depending on the access mode) have been read, the Latch
register unlatches and resumes following the Counting register.
## 7.15 THE LATCH COMMAND
To latch a channel, write a latch command byte to the Mode/Command register.
The latch command byte is cc000000 binary, where 'cc' is the channel number.
Then you can read the latched count from the data register for that channel.
The Latch register remains latched until it has been fully read, or until the
counter is reprogrammed with a new mode word. The latched value must be read
before any other operation is performed on the channel, except initialising
the channel with a new mode word.
{JAM} Latching the count in progress should not affect the Counting register
but when several machines were tested, they tended to occasionally miss a CTC
clock, i.e. fail to decrement, if latch commands were being issued. This was
much more pronounced on an Epson 386SX/20 PLUS, which would miss roughly one
clock for every two latch commands issued! This seems to be an isolated
example of bad hardware design, but is still disturbing.
The channel can also be latched via the read-back command (section »» 7.18).
The meaning of the value you read depends on the mode of the channel. The
meaning of the count in modes two and three are described in sections »» 7.15.1
and »» 7.15.2.
## 7.15.1 MEANING OF COUNT VALUE IN MODE TWO
In mode two, the value will be in the range of 1 to the divisor register value.
It will start at the divisor register value, and decrement down to 1. When it
would decrement to zero, it instead reloads to the divisor register value. For
example if the divisor was 5, the count sequence would be 5, 4, 3, 2, 1, 5, 4...
If the divisor is 0 (i.e. 65536), the sequence is 0, 65535, 65534, ... 2, 1, 0,
65535...
For channel zero, a rising edge on the output pin triggers the timer tick
interrupt at the instant that the channel reloads its Counting register from
the Reload register.
{JAM} On PS/2 machines, if the latch command is issued at the instant when the
Counting register changes from 1 to the reload value, occasionally the read will
yield a zero, even if the Reload register does not contain zero. In other
words, if the Reload register is 20, the count sequence would be 5, 4, 3, 2,
1, 20, 19, 18... At the instant between the 1 and the 20, the timer does
actually decrement to zero, and sometimes a zero will be read, even though
zero is not in the valid counting sequence.
If the divisor is 65536, the above problem mentioned by {JAM} does not occur.
In other cases, you could work around the problem by specifically checking for
a value of zero and substituting the reload value.
In mode two, if you are using a divisor of 65536 (the normal value for channel
zero), you can convert the down-counting value into an up-counting value by
performing a 16-bit negation, i.e. up_count = 0 - read_count0(); or neg ax (or
whichever register contains the count). This will give a 16-bit value which
increases from 0 to 65535 then back to 0 again.
If the divisor is not 65536, just subtract the count value from the divisor
value to get an up-counting value which will increase from 0 to divisor minus
one, then back to 0 again. See the above problem noted by {JAM}.
## 7.15.2 MEANING OF COUNT VALUE IN MODE THREE
Refer to section »» 7.8.5 for a description of the operation of mode three.
The raw count will always be an even value, because the Counting register
decrements in steps of two instead of steps of one. The behaviour with an
even divisor is easiest to describe, so I will assume that the divisor value
is even. In this case, the count register counts down from the divisor value,
in steps of two, until it reaches two, then reloads to the divisor value on the
next CTC clock. The output latch toggles state at this moment. For example,
if the divisor is 6, the count sequence would be 6, 4, 2, 6, 4, 2, 6, 4, 2...
with the output latch toggling at the transition between each '2' and '6'.
In this mode, to generate a full timestamp, you need to latch and read the
Counting register _and_ the output pin state, so you know whether the channel
is on its first or second countdown. The timer tick interrupt only occurs on
the rising edge of the output of the T flip-flop, at the end of every second
countdown (if we define the first countdown as when the output of the T flip-
flop is high, and the second countdown as when its output is low). The read-
back function is useful for this (it allows the count register and the output
state to be latched and read by software), and is described in section »» 7.18.
Mode two is more suitable than mode three for timestamping or timing functions,
because the Counting register behaves sensibly and there is no need to know
whether it is on the first or second countdown.
On machines that support readback (all AT-class machines except the PS/2, see
section »» 7.24.2), the count can therefore be read on-the-fly in mode three.
See section »» 7.20 for details.
See also section »» 7.15 for {JAM}'s comments on loss of CTC clocks when the
channel is latched or read-back.
## 7.16 SAMPLE CODE: READING THE COUNT IN MODE TWO
This function latches, reads, and returns the current Counting register contents
of CTC channel zero. Remember that the Counting register counts downwards.
This function assumes that CTC channel zero is operating in mode two with a
divisor of 65536. See section »» 7.10 and »» 7.12 for sample code to set the
mode and divisor.
See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-------------------------------- snip snip snip --------------------------------
/*
Function to latch and read the Counting register of CTC channel zero, assuming
that the channel is set to operate in mode two with a divisor of 65536.
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
*/
unsigned int read_channel0_mode2(void) {
unsigned int cv;
asm pushf; /* Preserve interrupt flag */
asm cli;
outportb(0x43, 0); /* Latch the count register */
cv = inportb(0x40); /* Lobyte of count */
cv += inportb(0x40) << 8; /* Hibyte of count */
asm popf; /* Restore interrupt flag */
return cv; /* Return down-counter */
}
-------------------------------- snip snip snip --------------------------------
## 7.17 THE LOBYTE/HIBYTE FLAG
Each timer channel has an internal flag which keeps track of whether the lobyte
or the hibyte of the count should be provided when the data port is read. Each
time the data port is read, this flag toggles state (unless the channel was
programmed for hibyte-only or lobyte-only access, i.e. bits 5 and 4 were 0,1
or 1,0 when it was initialised).
After programming a timer channel, the flag is clear, and reading the data port
will yield the lobyte, then the hibyte, then the lobyte, then the hibyte, etc.
But if some other badly-behaved software reads the data port only once (or any
odd number of times), the flag would be set, and you would read the hibyte
first, then the lobyte, so you would be out of sync with the counter. There is
no processor-accessible flag to tell you whether you are reading the lobyte or
the hibyte. Issuing a latch command doesn't affect the lobyte/hibyte flag,
either, unfortunately.
This is why it's essential to disable interrupts while accessing the CTC, and
always read or write BOTH bytes (unless the channel is programmed for lobyte-
only or hibyte-only access).
My experience has been that if you initialise the counter in your program
(initialising it clears the lobyte/hibyte flag), it will stay synchronised, and
there is no need to worry about the flag at all. If anyone has found otherwise,
please tell me about it. (*)
See section »» 7.27 for a program which attempts to determine the lobyte/hibyte
flag state (among other things).
## 7.18 THE READ-BACK COMMAND
The read-back command word is written to the mode/command register. Bits 7 and
6 of the command word (normally the counter select bits) are both '1'.
Read-back is not supported on the 8253 (PCs and XTs); it was added with the
8254 (AT and later). However, {JAM} says all AT documentation states that
this bit combination is reserved and, alas, the PS/2 LSI integration of the
CTC does not implement the read-back command - on a PS/2 the read-back command
is ignored. {JAM} has tested IBM ValuePoints and they are alright. It is
just the PS/2 that does not support read-back (see section »» 7.24.2).
A read-back command is specified by writing a value to the mode/command
register as follows:
7 6 5 4 3 2 1 0
1 1 . . . . . . (Specify read-back command)
. . * . . . . . Latch count flag: 0 = Yes, 1 = No
. . . * . . . . Latch status flag: 0 = Yes, 1 = No
. . . . * . . . Read-back timer channel 2: 1 = Yes, 0 = No
. . . . . * . . Read-back timer channel 1: 1 = Yes, 0 = No
. . . . . . * . Read-back timer channel 0: 1 = Yes, 0 = No
. . . . . . . 0 (Reserved for future expansion)
Command word bits 3, 2, and 1 enable read-back for timer channels 2, 1, and 0
respectively, thus any combination of the three channels can be selected for
read-back with one command word. Bits 5 and 4 enable the two types of
read-back. Important - Setting these bits to _zero_ enables the function.
Bit 5 specifies latching the count value. This is the same as issuing a counter
latch command (cc000000 binary), but several counters can be latched at the same
time, depending on which counters are enabled by bits 3, 2, and 1 of the
read-back command word.
Bit 4 specifies latching the channel status. If this function is enabled (by
setting the bit to 0), the next read of the data register for that channel will
yield a status read-back byte, which is defined as follows:
7 6 5 4 3 2 1 0
* . . . . . . . Output pin state
. * . . . . . . Null Count flag
. . * * . . . . Access mode as specified at initialisation
. . . . * * * . Operating mode as specified at initialisation
. . . . . . . * BCD flag as specified at initialisation
The bottom six bits return the values programmed into the channel when it was
last initialised by a write of a mode word. Bits 7 and 6 relate to real-time
events.
Bit 7 indicates the actual state of the output pin of the timer chip at the
moment that the read-back command was issued, and bit 6 indicates whether a
newly-programmed divisor value has been loaded into the Counting register yet
(if clear) or the channel is still waiting for a trigger signal or for the
Counting register to count down to zero before a newly programmed Reload value
is loaded into the Counting register (if set).
The bit is set upon a mode or Reload value write to the channel, and cleared
when the Reload value is loaded into the Counting register.
## 7.19 SAMPLE CODE: READ-BACK
This function performs a full read-back on a specified CTC channel and fills in
a readback_data structure with the count and the read-back status byte.
-------------------------------- snip snip snip --------------------------------
/*
Function to read-back a counter/timer channel count and status
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
*/
#pragma inline; /* Required for asm pushf, popf, and cli */
typedef struct {
unsigned int count;
unsigned char status;
} readback_data;
void readback_channel(unsigned int channum, readback_data * rbdp) {
if (channum < 3) {
asm pushf; /* Preserve interrupt flag */
asm cli; /* Disable interrupts */
outportb(0x43, 0xC0 + (2 << channum)); /* Latch count, status */
rbdp->status = inportb(0x40 + channum); /* Get status */
rbdp->count = inportb(0x40 + channum); /* Get count lobyte */
rbdp->count += inportb(0x40 + channum) << 8; /* Get count hibyte */
asm popf; /* Restore interrupt flag */
return;
}
}
-------------------------------- snip snip snip --------------------------------
## 7.20 READING THE COUNT IN MODE THREE (8254 ONLY)
Reading the count on-the-fly to get an absolute timestamp in mode three is
more awkward than reading the count in mode two, and has a higher overhead.
As far as I know, there is no reason why your program should not program CTC
channel 0 to operate in mode 2 and leave mode 2 in effect when your program
exits or is terminated (modern BIOSes set mode 2 as the default mode anyway,
see section »» 7.4.2), so there should be no requirement to be able to read
the count in mode 3. However, if the CTC is an 8254 (not an 8253 or a PS/2
CTC) it is possible to read the count in mode 3, so I will describe how this
is done.
The function presented in the section »» 7.21 is the result of some testing
and experimentation. I have found it to be reliable on all of the machines I
was able to test with, but if you have trouble with it, let me know. (*)
The basis of reading the count in mode three is to read the count value, and
also read the output pin state, then combine them. The count register counts
down in sequence 0, 65534, 65532, 65530 ... 8, 6, 2, 0, 65534, 65532... with the
output pin state toggling on each transition from 2 to 0. The rising edge of
the output pin will initiate a timer tick interrupt, therefore I regard this
as starting the count sequence, so when the output pin is low, the counter is
on its second pass. See sections »» 7.8.5 and »» 7.15.2 for more details.
We could use a read-back command to read the count and the output pin status,
and derive an up-count combined value as:
up_count = ((0 - actual_count) / 2) + (output_state ? 0 : 0x8000);
This will work, and is reliable on some machines, but on other machines, the
output pin state is occasionally read incorrectly, probably due to delays in
the logic of the timer chip. So, I had to modify the routine to read the output
pin state, read the count, read the output pin state again, then determine the
true count in progress.
The logic here is as follows: If the second output state is the same as the
first (this is nearly always the case), then the output state and the count are
both valid. If the output states are different, then a counter reload has
occurred during the reading process, so use the count value to determine whether
the count was latched just before, or just after, the output changed state.
If the count value (after converting to an up-count) is small, then it was read
just after the output changed state, so use the second output state. If the
count is large, then it was read just before the output changed state, so the
first output state is applicable.
Now that I have the correct output state, the equivalent up-count value can be
calculated using the above formula. This yields a 16-bit up-counting value
which corresponds to the negative of the equivalent raw count in mode two.
Needless to say, the part of the routine that talks directly to the timer chip
operates with interrupts locked out.
## 7.21 SAMPLE CODE: READING THE COUNT IN MODE THREE
This function latches, reads, and returns the current effective count value for
timer channel zero, converted to a 16-bit up-counting value. It works with
8254 CTCs and fully compatible ASICs, but does not work with 8253s or on PS/2
machines.
This function assumes that CTC channel zero is operating in mode three with a
divisor of 65536. This USED TO BE the default mode set up by the BIOS, but
mode 2 is the default used by modern 486 BIOSes that I have seen. See section
»» 7.4.2 for details. The function also assumes that channel zero is set for
lobyte/hibyte access (bits b5,4 = 1,1 in control register at initialisation)
and that the lobyte/hibyte flag is correctly synchronised (see sections »» 7.7
and »» 7.17.
See section »» 7.12 for sample code to set the mode and divisor.
-------------------------------- snip snip snip --------------------------------
/*
Function to read the count register (down-counter) of timer channel zero,
assuming that the timer is in mode three, with a divisor of 65536.
Returns the count in up-counter format. Requires an 8254 timer chip.
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
*/
unsigned int read_timer0_mode3(void) {
unsigned char st1, st2; /* Status read-back values */
unsigned int cv; /* Count value */
disable(); /* No ints please - can use asm cli */
outportb(0x43, 0xE2); /* Latch and read back status byte */
st1 = inportb(0x40); /* Read status byte */
outportb(0x43, 0x00); /* Latch count for timer 0 */
cv = inportb(0x40); /* Lobyte of count */
cv += inportb(0x40) << 8; /* Hibyte of count */
cv = (0 - cv) >> 1; /* Convert to up-count, 0-32767 */
outportb(0x43, 0xE2); /* Latch and read back status byte */
st2 = inportb(0x40); /* Read status byte */
enable(); /* Ints back on - can use asm sti */
if ((st1 ^ st2) & 0x80) /* If output pin changed state... */
if (cv < 0x4000) /* If reload just occurred... */
st1 ^= 0x80; /* Use newer output pin status */
if ((st1 & 0x80) == 0) /* If on second countdown... */
cv |= 0x8000; /* Set b15 */
return cv; /* Return as up-counter */
}
-------------------------------- snip snip snip --------------------------------
## 7.22 SAMPLE CODE: OPTIMISED MODE THREE COUNT READING FUNCTION
The following function reads the count register of CTC channel zero assuming
that CTC channel zero is operating in mode three with a divisor of 65536 and
is set for lobyte/hibyte access, and the lobyte/hibyte flag is correctly
synchronised.
The value is returned in up-counting format, in the range 0-65535, and is
the effective value that would be read from the counter in mode two using a
raw read, except that the counting direction is reversed (the value returned
by this function is an up-counter, the raw value is a down-counter).
-------------------------------- snip snip snip --------------------------------
; Function to read the count register (down-counter) of CTC channel zero,
; assuming that the channel is in mode three, with a divisor of 65536.
; Returns the count in up-counter format. Requires an 8254 timer chip.
; Part of the PC Timing FAQ / Application notes
; By K. Heidenstrom (kheidens@actrix.gen.nz)
;
_read_timer0_mode3 PROC near ; or FAR for far code model
; unsigned int read_timer0_mode3(void);
pushf ; Keep interrupt flag
mov al,11100010b ; Latch and read back status byte only
cli ; Lock out interrupts
out 43h,al ; Send it
jmp SHORT $+2 ; Delay
in al,40h ; Get status byte
mov ah,al ; To AH
jmp SHORT $+2 ; Delay
mov al,00000000b ; Latch count for timer 0
out 43h,al ; Send it
jmp SHORT $+2 ; Delay
in al,40h ; Get lobyte of count
mov dl,al ; Save in DL
jmp SHORT $+2 ; Delay
in al,40h ; Get hibyte of count
mov dh,al ; Save in DH
jmp SHORT $+2 ; Delay
mov al,11100010b ; Latch and read back status byte again
out 43h,al ; Send it
jmp SHORT $+2 ; Delay
in al,40h ; Get status byte
popf ; Restore interrupt flag
neg dx ; Convert to ascending count
xor al,ah ; Did the output change?
jns GotCount ; If not, no problemo
test dh,dh ; Was count high or low?
js GotCount ; If count was about to carry, keep old
not ah ; If count just carried, change output
GotCount: shl ah,1 ; Get output pin status to CF
cmc ; Pin high = count 0-32767
rcr dx,1 ; Pin low = count 32768-65535
ret ; Return 16-bit ascending count in DX
_read_timer0_mode3 ENDP
-------------------------------- snip snip snip --------------------------------
## 7.23 SAMPLE PROGRAM: MANIPULATE THE CTC AND PORT B
The following program is a command driven utility that manipulates the CTC and
the Port B hardware. It lets you send commands to the mode/command register,
read and write the data registers in single-byte or lobyte/hibyte modes, set
and display the Timer 2 Gate and Speaker Gate signals, and read the Timer 2
output on port B or C (see section »» 7.5). It has a simple help summary which
is displayed when '?' is entered at the prompt. The program performs minimal
error checking and is not intended to be bulletproof. You may find it useful
for testing some subtle details of the CTC's operation.
Parameters to a command must be separated from the command name by one or more
spaces or tabs. Commands on the same line may be separated by semicolons (;)
and the whole command line will be executed with interrupts locked out. Result
text is stored in an internal buffer and displayed once the command line has
been fully processed.
Numeric parameters are assumed to be binary by default. To specify a hex value,
prefix the hex digits with 'x' (e.g. 'xFEDC'). To specify a decimal value,
prefix the digits with 'd' (e.g. 'd12345').
-------------------------------- snip snip snip --------------------------------
/*
Sample program #6
Utility to manipulate the CTC
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save this file to SAMPLE6.C and compile with:
bcc -I<inc_path> -L<lib_path> -ms sample6.c
Where inc_path is the path to your C header files and your startup modules
C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, and cli */
#include <ctype.h> /* For tolower() */
#include <dos.h> /* For inportb() and outportb() */
#include <io.h> /* For read() and write() */
#include <stdio.h> /* For printf() */
#include <stdlib.h> /* For exit() */
#include <string.h> /* For strlen() */
#define FALSE 0
#define TRUE 1
#define STDIN 0
#define LINELEN 120 /* Line length limit */
static unsigned int eval_ok;
static char resulttext[10240]; /* Buffer for result text */
static char * resulttextp;
unsigned int eval_value(char * s) {
unsigned int p, v;
char c;
p = v = 0;
eval_ok = TRUE;
if (s[0] == 'd') { /* Decimal value */
++p;
while ((c = s[p++]) > ' ') {
v *= 10;
if ((c >= '0') && (c <= '9'))
v += (c - '0');
else
return (eval_ok = FALSE);
}
return v;
}
if (s[0] == 'x') { /* Hex value */
++p;
while ((c = s[p++]) > ' ') {
v <<= 4;
if ((c >= '0') && (c <= '9')) {
v += (c - '0');
continue;
}
if ((c >= 'a') && (c <= 'f')) {
v += (c - 'a' + 10);
continue;
}
return (eval_ok = FALSE);
}
return v;
}
while ((c = s[p++]) > ' ') { /* Binary value - default */
v <<= 1;
if ((c == '0') || (c == '1'))
v += (c - '0');
else
return (eval_ok = FALSE);
}
return v;
}
void rw_reg(unsigned int is16, unsigned int chan, char * parms) {
unsigned int ioadr, v;
ioadr = 0x40 + chan;
if (parms[0]) {
v = eval_value(parms);
if (eval_ok == FALSE) {
sprintf(resulttextp, "Bad parameter value: '%s'\n", parms);
resulttextp = resulttext + strlen(resulttext);
return;
}
outportb(ioadr, v & 0xFF);
if (is16)
outportb(ioadr, v >> 8);
return;
}
v = inportb(ioadr);
if (is16) {
v += (inportb(ioadr) << 8);
sprintf(resulttextp, "Channel %d read lobyte/hibyte: 0x%04X\n", chan, v);
}
else
sprintf(resulttextp, "Channel %d read byte: 0x%02X\n", chan, v);
resulttextp = resulttext + strlen(resulttext);
return;
}
void do_command(char * cmd, char * parms) {
unsigned int v;
switch (cmd[0]) {
case '?' :
sprintf(resulttextp,
"Command format: cmd [parms] [; cmd [parms]] [...]\n\n"
"Commands on the same line are executed with interrupts locked out\n"
"Values may be hex ('x' prefix), decimal ('d' prefix) or binary (default)\n"
"\nCommands are:\n\n"
"0 [value] - read [write] channel 0 data register\n"
"1 [value] - read [write] channel 1 data register\n"
"2 [value] - read [write] channel 2 data register\n"
"00 [value] - read [write] channel 0 data register as lobyte/hibyte\n"
"11 [value] - read [write] channel 1 data register as lobyte/hibyte\n"
"22 [value] - read [write] channel 2 data register as lobyte/hibyte\n"
"C value - write value to mode/command register\n"
"R - read back timer 2 output via port B or C\n"
"G [on|off] - read [set] timer 2 gate on port B\n"
"S [on|off] - read [set] speaker gate on port B\n"
"Q - quit\n"
"\nExample command: g on; c 10110110; 22 x1234; s on\n"
);
resulttextp = resulttext + strlen(resulttext);
break;
case '0' :
case '1' :
case '2' :
if (cmd[1] == cmd[0])
rw_reg(TRUE, cmd[0] - '0', parms);
else
rw_reg(FALSE, cmd[0] - '0', parms);
break;
case 'c' :
if (!parms[0]) {
sprintf(resulttextp, "Must give parameter for 'c' command\n");
resulttextp = resulttext + strlen(resulttext);
return;
}
v = eval_value(parms);
if (eval_ok == FALSE) {
sprintf(resulttextp, "Bad parameter value: '%s'\n", parms);
resulttextp = resulttext + strlen(resulttext);
return;
}
outportb(0x43, v & 0xFF);
break;
case 'r' :
sprintf(resulttextp, "Timer 2 readback on port B (AT) is %s; on port C (PC/XT) is %s\n",
(inportb(0x61) & 0x20) ? "high" : "low",
(inportb(0x62) & 0x20) ? "high" : "low");
resulttextp = resulttext + strlen(resulttext);
break;
case 'g' :
if (parms[0])
outportb(0x61, (inportb(0x61) & 0xFE) | (parms[1] == 'n'));
else {
sprintf(resulttextp, "Timer 2 gate is currently %s\n",
(inportb(0x61) & 0x01) ? "on" : "off");
resulttextp = resulttext + strlen(resulttext);
}
break;
case 's' :
if (parms[0])
outportb(0x61, (inportb(0x61) & 0xFD) | ((parms[1] == 'n') << 1));
else {
sprintf(resulttextp, "Speaker gate is currently %s\n",
(inportb(0x61) & 0x02) ? "on" : "off");
resulttextp = resulttext + strlen(resulttext);
}
break;
case 'q' :
asm sti;
exit(0);
default :
if (parms[0])
sprintf(resulttextp, "Bad command: '%s %s'\n", cmd, parms);
else
sprintf(resulttextp, "Bad command: '%s'\n", cmd);
resulttextp = resulttext + strlen(resulttext);
}
return;
}
void do_commandline(char * s) {
static char cmdbuf[LINELEN];
static char parmbuf[LINELEN];
unsigned int sp, dp1, dp2, endflags;
char c;
resulttextp = resulttext;
asm cli;
sp = 0;
do {
dp1 = 0; dp2 = 0;
while ((s[sp] <= ' ') && (s[sp] != '\0'))
++sp; /* Skip leading whitespace */
while ((s[sp] > ' ') && (s[sp] != ';')) {
c = s[sp++];
cmdbuf[dp1++] = tolower(c);
}
cmdbuf[dp1] = '\0';
if ((s[sp] != '\0') && (s[sp] != ';')) {
while ((s[sp] <= ' ') && (s[sp] != '\0'))
++sp; /* Skip whitespace */
while ((s[sp] != '\0') && (s[sp] != ';')) {
c = s[sp++];
parmbuf[dp2++] = tolower(c);
}
}
while (dp2) {
if (parmbuf[dp2 - 1] <= ' ')
--dp2;
else
break;
}
parmbuf[dp2] = '\0';
if (dp1)
do_command(cmdbuf, parmbuf);
if (s[sp] == ';')
++sp;
} while (s[sp]);
asm pushf;
asm pop endflags;
asm sti;
if (resulttextp != resulttext)
write(1, resulttext, strlen(resulttext));
if (endflags & 0x200)
printf("\nWarning! Interrupts were inadvertently enabled during the command!\n");
}
void main(void) {
static char inpbuf[LINELEN];
unsigned int p;
printf("Sample program #6 - Manipulates the CTC directly\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
printf("Type '?' for help, 'Q' to quit\n");
while (1) {
printf("\n>");
if ((p = read(STDIN, inpbuf, LINELEN - 2)) > 0)
--p;
inpbuf[p] = '\0';
do_commandline(inpbuf);
}
}
-------------------------------- snip snip snip --------------------------------
## 7.24 HARDWARE PROBLEMS AND DIFFERENCES
## 7.24.1 DIFFERENCES BETWEEN THE INTEL 8253 AND 8254
Though the 8254 was a "completely new design" from the 8253, the differences to
the user or programmer are that the 8254 has the read-back command (see section
»» 7.18), and the 8254 fixes a problem on the 8253 when used in mode 3 with a
reload value of 3 (which does not concern us).
## 7.24.2 CHIPSET IMPLEMENTATIONS
Differences in timer implementations in chipset ASICs are likely to be vague
and unpredictable. Prof. John Mertus {JAM} (see section »» 1.7) has done some
research on this, and found some machine-specific hardware differences. These
are described in the applicable sections here, indicated with the marker {JAM}.
One thing John discovered is that the PS/2 ASIC does not implement the read-back
function (see section »» 7.18). Personally, I am p*ssed off at IBM for making
such a cretinous and inconsiderate mistake. Because of them, we cannot just
look at the machine type byte in the ROM and be sure that, if the machine is an
AT-class machine, read-back will work - we must specifically test whether the
machine supports read-back, and our programs may have to behave differently
depending on the result of the test. Normally, clones are criticised for not
being fully IBM compatible - this time, it is IBM! Rant mode off, dismount :-)
BTW, {JAM} also reports that the 8254 CTC is implemented properly on the IBM
ValuePoints. Any information on other machines would be welcomed. (*)
## 7.24.3 INTEL 8253/8254/82C54 CLOCK SYNCHRONISATION PROBLEMS
This information is from Intel Q&A and application notes, and was sent to me by
Louis Warshaw (louis@gate.net). Thanks Louis!
Unfortunately I found the Intel documentation very vague, so I will quote the
relevant parts and hope that Intel don't sue me :-) The problems concern
synchronisation between the CTC clock input (the 1.193182 MHz clock) and the
write access pulses when the data registers are written or when a counter latch
command is issued.
-WR ────────────────┐ ┌───────────────────────────
└───────┘
^a
CTC Clk ┌───────────────────
────────────────────────────────┘
^b
The timing diagram shows a write access to a data register (I/O address 40h,
41h, or 42h) and a rising edge on the CTC clock. The chip's specification for
the time between point 'a' and point 'b' is called Twc, and is specified as 55
nanoseconds maximum for the Intel 8254.
Here is what the Intel documentation says. My comments are in square brackets.
"Question: Why is Twc specified to the rising edge of [CTC] Clock, but
yet Clocks are loaded [sic] on the falling edge?
"Answer: This is used for software synchronisation of loading a new
count [reload value]. The new value must be in the Twc window
to guarantee that the new count [reload value] is loaded on the
next falling edge [of CTC clock]."
I think this is just saying that the reload register must be fully loaded before
the rising edge of CTC Clock, in order to be decremented on the following
falling edge of CTC Clock. I assume that if the reload register is not loaded
at least Twc nanoseconds before the rising edge, the chip will just wait for the
next rising edge, thus there is an uncertainty of one CTC Clock width as to
exactly _which_ CTC Clock will start decrementing the counting register, and
this depends on the reload register becoming fully loaded at least shortly
before the rising edge before the falling edge that will decrement it.
"Question: Why should Gate be pulsed immediately following a write of a
new count [reload] value, when using an asynchronous clock
source [CTC Clock not synchronous with the Write pulse] in
modes two and three?
"Answer: If an asynchronous clock input is used for a counter [channel],
you need to use Gate to synchronise the loading of the new count
[reload value]."
As for the second point, Intel's question and answer are so vague that I can
not come to any conclusion about the implications for the programmer.
"Question: What does the comment on page 3-74, figure 17, Note,
Peripheral Components, 1993 mean? "NOTE: A Gate transition
should not occur one [CTC] clock prior to terminal count".
"Answer: Modes 2 and 3 use the [CTC] clock frequency for the Rate
Generator and Square Wave Mode respectively. In modes 2 and
3, the 8254 (and 82C54) uses "look ahead" logic to precondition
OUT to go low on the falling edge of the CLK input upon
terminal count. Without this look ahead feature, the 8254
would not have time to resolve its internal logic at the same
time OUT is to go low upon reaching terminal count. Monitoring
the count value in software, before disabling counting via the
Gate, is usually sufficient to prevent this combination of
events. This has always been the operation of the 8254 (and
8253, and 82C54) and no problems resulting from this [sic]."
Again nice and vague. I think this is saying that terminal count is anticipated
by the look-ahead logic one CTC clock before it actually occurs, i.e. in mode 2
when the Counting Register reaches two, and if Gate goes low while the Counting
Register is two, the output may actually go low as normal on the next CTC clock
even though the Gate input is low. I wonder how this relates to mode 3.
Two more problems are described. These apply only to the 82C54, the CMOS
version of the 8254. I do not know whether any PCs actually use the 82C54.
There are two 'failure modes' documented - the Twc count write failure mode and
the Tcl counter latch command failure mode.
"The Twc [counter write] failure mode occurs in a very narrow window
between the Twc min and Twc max timing when writing the last [or only]
byte of a count [Reload register] value. The Twc specification defines
the relationship between the writing of a count [Reload] value and the
Clk [CTC clock] pulse and whether the Clk pulse will or will not be
reflected in the subsequent counting operation. The Clk pulse is a
low to high transition on one of the 82C54's Clk input pins.
[The 82C54 documentation states Twc min = 0, Twc max = 55 ns].
-WR ────────────────┐ ┌───────────────────────────
└───────┘
│<─Twc─>│
CTC Clk ┌───────────────────
────────────────────────────────┘
"If the rising edge of a Clk pulse happens before the Twc min
specification then it is too early and will not be reflected in
the count. If the Clk pulse happens after the Twc max specification
then the Clk pulse will be reflected in the count. If the Clk happens
between Twc min and Twc max it may or may not be reflected in the count
value.
Twc min is 0 ns and Twc max is 55 ns or a 55 ns window [sic].
"There is a worst case 8-20 ns [floating] window between Twc min and
Twc max where the 82C54 counter control logic is corrupted and the
counter enters an undefined state. The counter must be re-initialised
by rewriting the counter Mode word. The problem is worse at cold
temperatures (0 degrees C) and low VCC (4.5V).
Only the counter being written to is affected.
The other counters continue to count properly.
"The Twc failure mode actually varies across the normal skew of the
fabrication process. The 82C54's typical wafer fabrication process
failure mode window is between 300 picoseconds to 1 nanosecond. The
actual window may typically be less but this represents the +/- 100
picosecond resolution of the Teradyne test computer used to characterise
the Twc failure mode. When the process shifts within the normal skew to
the slow implant corner the failure mode window increased [sic] to a
worst case of 8-20 nanoseconds.
"The failure mode is a function of an asynchronous Clk and -WR input
signals. When -WR and Clk are asynchronous the -WR may occur at any
time in relation to the Clk. If -WR and Clk are synchronous -WR will
always occur in the same relation ship [sic :-)] to Clk. The 82C54
Clk and -WR inputs are synchronous when the Clk input is the system
microprocessor clock, or a derivative of it. If the 82C54 Clk source
is independent of the system clock then the -WR and Clk are
asynchronous unless hardware synchronised external [sic] to the 82C54.
"There are three modifications which compensate for the failure mode:
"1. Use a Clk input signal which is a derivative of the system
microprocessor clock source. This makes the interaction of the
-WR and Clk totally predictable. The -WR and Clk will not happen
coincidentally and the synchronisation prohibits occurrence of a
-WR within the failure mode window time of Clk.
"2. Through the use of the 82C54 Read Back Command the software
detects the state of the Counter Status byte Null Count flag which
indicates whether the count has been moved from the Count Register
[Reload register] to the Counting Element (CE) [Counting register]
or "loaded". See Figure 1 Internal Block Diagram of a Counter (
Figure 5, 82C54 Data Sheet). Unless the Null Count flag is cleared
the count has not been successfully loaded. If the Null Count flag
is not cleared then the software rewrites the Mode word and count
value [Reload value].
"3. Externally synchronise the -WR and Clk input signals. This is done
by gating -WR with Clk. The -WR and Clk inputs then appear
synchronous to the 82C54 which prohibits the occurrence of a -WR
within the failure mode window time of Clk."
As far as I can tell from discussion with Louis Warshaw, the problem affects
writing a reload value on-the-fly to a CTC channel. The -WR signal on the
timing diagram represents the pulse issued by the processor to write the last
or only byte of the new reload value to the channel. The problem occurs if
the rising edge of the Clk to that channel occurs within a certain time of the
trailing (rising) edge of the write. There is a timing window which is between
8 and 20 ns wide, and may dynamically shift within the 0 to 55 ns specification
Twc window, relative to the rising edge of -WR. If a rising edge of Clk appears
within this 8 to 20 ns wide window, the internal logic of the counter will be
corrupted and the counter will go into never-never land until reinitialised by
a Mode write to the Mode/Command register.
"Counter Latch Command failure mode, Tcl
"The failure mode occurs during a very narrow window between -WR and
Clk when latching a count [Counting register] value. The approximately
10 nanosecond window between -WR rising edge and -Clk falling edge, when
asynchronously writing a Counter Latch or Read Back command, the count
value read may be in error. The byte value read is not in sequence in
relation to the previous or following byte read. The Counting Element
[Counting register] and counter control logic are unaffected by the
failure mode and continues [sic] to decrement properly.
-WR ────────────────────────────┐ ┌───────────────
└───────┘
│<───Tcl───>│
CTC Clk ────────────────────────┐
└───────────────────────────
"The error window has been verified on a Teradyne test computer to be a
200-300 picoseconds [sic] window between -WR rising edge and Clk falling
edge when writing a Counter Latch or Read Back command.
"The failure mode is not a violation of the Tcl specification. The Tcl
specification tells the user a Clk pulse falling edge which happens
close to the -WR rising edge of a Counter Latch or Read Back command
will (Tcl min) or will not (Tcl max) be reflected in the count value
subsequently read from the Counter Output Latch [Latch register]. The
Tcl specification provides for a +/- one Clk pulse, or one bit error,
in the count value latched. The failure mode results in a multiple bit
error in the count value read [from the Latch register].
"There are three modifications which compensate for the failure mode:
"1. Use a Clk signal which is a derivative of the system microprocessor
clock source. This makes the interaction of the -WR and Clk [sic]
totally predictable. The -WR and Clk never happen coincidentally
and the synchronisation prohibits occurrence of a WRX [sic] within
the failure mode window time of Clk.
"2. Latch and read the count twice if an error greater than one bit
error occurs.
"3. Externally synchronise the -WR and Clk input signals. This is done
by gating -WR with Clk. -WR and Clk then appear synchronous to the
82C54 which prohibits the occurrence of a -WR within the failure
mode window time of Clk.
This is saying that if the -WR access on a counter latch or read-back command
falls within a narrow window a certain length of time after the falling edge of
the Clk to that channel, an incorrect count value is latched in the Latch
register. Presumably this occurs because the value provided by the Counting
register becomes briefly invalid a short time after the Counting register
decrements, and if the latch command happens to occur during that short time,
the invalid value will be latched into the Latch register. Thus, with an 82C54
where the Clk is not synchronous with -WR, you cannot trust the value latched
by a Counter Latch or read-back command.
## 7.25 IS THE CTC AN 8253 OR AN 8254?
Well, you can check the BIOS Machine Type Byte at location F000:FFFE. Values
0xFD, 0xFE, and 0xFF indicate PCjr, XT/Portable, and original PC respectively,
all of which have 8253 CTCs. A Type Byte value of 0xFC indicates an AT or
later machine, which should have an 8254. In other words,
-------------------------------- snip snip snip --------------------------------
unsigned int is_machine_an_AT(void) {
return (*(unsigned char far *)MK_FP(0xF000, 0xFFFE) == 0xFC);
}
-------------------------------- snip snip snip --------------------------------
However, this method is not foolproof, because some clones may not have a valid
Machine Type byte, and because of IBM's brilliance in not implementing a proper
8254 in the PS/2's ASIC. With this test, some clones, and PS/2 machines, would
report an 8254 when they may only have an 8253. Also, when the CTC is emulated,
as it is for DOS applications running under OS/2, and presumably under Linux,
the full functionality of the CTC may not be available.
The sample program in section »» 7.26 contains some code that determines
whether the CTC is an 8253 or an 8254 or something else (i.e. a faulty chip or
a partially emulated chip) which can be extracted and used to determine the CTC
type, but note that it leaves CTC channel two in a non-standard mode (mode 0).
This should not be a problem, as channel two is used for audio generation and
is fully initialised by any code that will subsequently use that channel.
## 7.26 DETERMINING THE EXACT STATE OF THE CTC
You might need to determine the state of a particular channel in the timer chip.
The only things you can really find out are the state of the lobyte/hibyte flag
(this cannot be read directly, but its state can be inferred by reading the
count several times, assuming the channel is being clocked), and the programmed
operating mode and BCD/binary flag, which can be determined by the read-back
command (assuming that the timer chip is an 8254).
The logic of the function infer_lobyte_hibyte_flag() in the program in section
»» 7.27 is: read the count, then repeatedly re-read the count until a different
value is obtained. Then infer the state from whether the lobyte or the hibyte
of the value has changed. If the lobyte changed, then the flag is in sync
(normal). If the hibyte changed, then the flag is out of sync. In the latter
case, the flag can be brought into sync by reading the data register once.
For each counter read operation, the count is latched prior to being read.
The routine disables interrupts, to ensure that the number of CTC clocks
between reads is minimal.
## 7.27 SAMPLE PROGRAM: REPORT CHANNEL STATES
-------------------------------- snip snip snip --------------------------------
/*
Sample program #7
Reports CTC type (8253, 8254, or faulty/emulated) and the operating states
(lobyte/hibyte flag, mode, binary/BCD, output state) of all channels.
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save this file to SAMPLE7.C and compile with:
bcc -I<inc_path> -L<lib_path> -ms sample7.c
Where inc_path is the path to your C header files and your startup modules
C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, and cli */
#include <bios.h>
#include <dos.h>
#include <process.h>
#include <stdio.h>
#include <stdlib.h>
#define TESTVALUE 0x55AA /* Value to use as reload test value */
#define BACKWARDS ((unsigned int)((TESTVALUE >> 8) + ((TESTVALUE & 0xFF) << 8)))
/* Backwards TESTVALUE */
#define EXP_CSTAT 0x30 /* Expected counter status */
#define CTC_EMUL 0 /* CTC is faulty or emulated by OS */
#define CTC_8253 1 /* CTC is an 8253 */
#define CTC_8254 2 /* CTC is an 8254 */
#define LHF_INSYNC 0 /* Lobyte/hibyte flag is in sync */
#define LHF_OUTSYNC 1 /* Lobyte/hibyte flag is out of sync */
#define LHF_UNKNOWN 2 /* Lobyte/hibyte flag cannot be determined */
typedef struct {
unsigned int count;
unsigned char status;
} readback_data;
/* Code */
unsigned int read_channel_raw(unsigned int channum) {
unsigned int cv;
if (channum < 3) {
asm pushf;
asm cli;
outportb(0x43, channum << 6);
cv = inportb(0x40 + channum);
cv += inportb(0x40 + channum) << 8;
asm popf;
}
return cv;
}
/* Simple short delay - just wait for at least one CTC clock to occur */
void wait_ctc_clock(void) {
unsigned int ch0count;
ch0count = read_channel_raw(0);
while (read_channel_raw(0) == ch0count)
;
return;
}
/* The following function is described in section »» 7.18 */
void readback_channel(unsigned int channum, readback_data * rbdp) {
if (channum < 3) {
asm pushf; /* Preserve interrupt flag */
asm cli; /* Disable interrupts */
outportb(0x43, 0xC0 + (2 << channum)); /* Latch count, status */
rbdp->status = inportb(0x40 + channum); /* Get status */
rbdp->count = inportb(0x40 + channum); /* Get count lobyte */
rbdp->count += inportb(0x40 + channum) << 8; /* Get count hibyte */
asm popf; /* Restore interrupt flag */
return;
}
}
/* This function determines the CTC type. It stores the current contents of
Port B (speaker and timer 2 gate control port), then turns off speaker
enable and sets timer 2 gate low. It then attempts to read-back the
status of CTC channel 2, and stores this in ch2rbd.status; this will be
used to restore channel 2 to its original operating state if it turns out
that the CTC is an 8254.
The function then programs CTC channel 2 for mode zero with a reload value
specified by TESTVALUE. In this mode, channel 2 will reload on the next
CTC clock, and will not decrement, as its gate input is low. We then wait
for at least one CTC clock to occur (detect this by reading CTC channel 0
and waiting for a change in the latched value). CTC channel 2 then contains
a known value, and is in a stable state. We know the expected latched count
value and the expected status value to be returned on a read-back. It is
then possible to determine the CTC type by reading a few things and looking
at the CTC's responses. Specifically, the routine checks that it can latch
and read a stable value equal to the reload register value - if this fails,
the CTC is assumed to be faulty or emulated. Then it issues a read-back
command and keeps the read-back status byte, then latches and reads the
count. If the CTC is an 8253, this will yield a 'backwards' count - i.e.
TESTVALUE with hibyte and lobyte interchanged, because the lobyte/hibyte
flag was reversed by the read-back (which reads the data register three
times). On an 8254, this latch and read will yield TESTVALUE.
Next, it performs another readback (to reinstate the original lobyte/hibyte
flag if the CTC is an 8253) and keeps the read-back status again, then it
latches and reads the count again. This should _always_ yield TESTVALUE,
for either an 8253 or 8254.
It then checks for the expected behaviour of an 8253 and an 8254 separately.
If the CTC does not give the correct response, it will be reported as faulty
or emulated.
Note that this function spends most of its time with interrupts locked out.
*/
unsigned int detect_ctc_type(void) {
unsigned int ctctype; /* Value to be returned */
unsigned int port61; /* Port 61h value */
readback_data ch2rbd, rbd1, rbd2; /* Read-back storage */
unsigned int backwards, forwards; /* Latched count values */
ctctype = CTC_EMUL; /* Assume faulty or unknown CTC type */
asm pushf;
asm cli;
port61 = inportb(0x61); /* Get Port B value */
outportb(0x61, port61 & 0xFC); /* Turn off timer 2 gate and speaker */
/* Try read-back on channel two, only useful if CTC turns out to be an 8254 */
readback_channel(2, &ch2rbd);
readback_channel(2, &ch2rbd); /* Attempt to read-back channel two */
outportb(0x43, 0xB0); /* Channel 2, two bytes, mode 0, binary */
outportb(0x42, TESTVALUE & 0xFF); /* Lobyte of reload value */
outportb(0x42, TESTVALUE >> 8); /* Hibyte of reload value */
wait_ctc_clock();
wait_ctc_clock(); /* Wait for a couple of CTC clock pulses */
/* Just read the raw value a couple of times, make sure it's stable */
if ((read_channel_raw(2) != TESTVALUE) ||
(read_channel_raw(2) != TESTVALUE))
goto got_type; /* Structured programming? Never heard of it */
/* Try a read-back - on an 8253, this will reverse the lobyte/hibyte flag */
readback_channel(2, &rbd1);
/* Read the count - on an 8253 this will be TESTVALUE backwards, on an 8254
it will be TESTVALUE */
backwards = read_channel_raw(2);
/* Try another read-back, into rbd2 this time */
readback_channel(2, &rbd2);
/* Now latch and read the count again */
forwards = read_channel_raw(2);
/* Now, try to figure out what it is! */
if ((rbd1.status != EXP_CSTAT) && (rbd2.status != EXP_CSTAT) &&
(backwards == BACKWARDS) && (forwards == TESTVALUE))
ctctype = CTC_8253;
if ((rbd1.status == EXP_CSTAT) && (rbd2.status == EXP_CSTAT) &&
(backwards == TESTVALUE) && (forwards == TESTVALUE))
ctctype = CTC_8254;
got_type:
/* Now we know what it is. If it's an 8254, we can restore channel 2 to its
previous mode, although we cannot restore the original divisor, because
we can't tell what it was. If it's not an 8254, we can't fix anything */
if (ctctype == CTC_8254) {
outportb(0x43, 0x80 + (ch2rbd.status & 0x3F));
outportb(0x42, 0);
outportb(0x42, 0);
}
outportb(0x61, port61); /* Restore speaker and timer 2 control bits */
asm popf;
return ctctype;
}
unsigned int test_delta(unsigned int latchv, unsigned int cport) {
unsigned int nreads, startcount, count, diff, hbdiff, lbdiff;
asm pushf;
asm cli;
outportb(0x43, latchv); /* Read count to startcnt */
startcount = inportb(cport);
startcount += inportb(cport) << 8;
for (nreads = 0; nreads < 20; ++nreads) {
outportb(0x43, latchv); /* Latch count again */
count = inportb(cport);
count += inportb(cport) << 8;
diff = startcount ^ count; /* Get difference */
hbdiff = ((diff & 0xFF00) != 0);
lbdiff = ((diff & 0x00FF) != 0);
if (lbdiff == hbdiff) /* Both or neither changed */
continue; /* Wait for difference */
if (lbdiff) { /* Lobyte changed */
asm popf;
return LHF_INSYNC; /* Flag is in sync */
}
if (hbdiff) { /* Hibyte changed */
asm popf;
return LHF_OUTSYNC; /* Flag is out of sync */
}
} /* for nreads */
asm popf;
return LHF_UNKNOWN; /* Couldn't determine */
}
/* The following function infer_lobyte_hibyte_flag() attempts to determine the
state of the lobyte/hibyte flag for a specified CTC channel, assuming that
that channel has been programmed for lobyte/hibyte access (i.e. bits 5 and 4
of the control register were 1,1 at initialisation). It should work for
both the 8253 and 8254. The lobyte/hibyte flag toggles every time the
latched (or unlatched) count is read from the counter data register. The
function returns LHF_INSYNC, LHF_OUTSYNC, or LHF_UNKNOWN. This function
seems to be reliable on fast machines but does not seem to work well on
slow machines or XTs (I don't know why), so don't rely on its accuracy! */
unsigned int infer_lobyte_hibyte_flag(int channum) {
unsigned int latchv, cport, result;
unsigned int progress[3];
if (channum > 2)
return LHF_UNKNOWN;
latchv = channum << 6;
cport = 0x40 + channum;
progress[LHF_INSYNC] = 0;
progress[LHF_OUTSYNC] = 0;
progress[LHF_UNKNOWN] = 0;
do
result = test_delta(latchv, cport);
while (++progress[result] < 10);
return result;
}
void main(void) {
unsigned char machtype; /* Machine type byte */
readback_data rbd[3]; /* Readback data structures */
unsigned int lhf[3]; /* Lobyte/hibyte flag values */
unsigned int ch; /* Channel number */
unsigned int port61; /* Port 61h value */
static char machname[4][31] = {
"an AT class machine (8254 CTC)", /* 0xFC */
"a PCjr (8253 CTC)", /* 0xFD */
"a PC/XT (8253 CTC)",
"an IBM-PC (8253 CTC)"
};
printf("Sample program #7 - Reports CTC type, modes, and output states\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
machtype = *(unsigned char far *)MK_FP(0xF000, 0xFFFE);
if (machtype < 0xFC)
printf("The BIOS Machine Type byte has a non-standard value\n\n");
else
printf("The BIOS Machine Type byte says this machine is %s\n\n", machname[machtype - 0xFC]);
switch (detect_ctc_type()) {
case CTC_EMUL:
printf("CTC appears to be faulty, non-standard, or emulated by operating system\n\n");
printf("Cannot determine operating parameters\n");
break;
case CTC_8253:
printf("CTC is an 8253\n\nCannot determine operating modes; attempting to determine\n");
printf("lobyte/hibyte flag state assuming lobyte/hibyte access and mode 2 or 3\n\n");
for (ch = 0; ch < 3; ++ch) {
switch (infer_lobyte_hibyte_flag(ch)) {
case LHF_INSYNC:
printf("Channel %d lobyte/hibyte flag sync:\tCorrect\n", ch);
break;
case LHF_OUTSYNC:
printf("Channel %d lobyte/hibyte flag sync:\tReversed\n", ch);
break;
default:
printf("Channel %d lobyte/hibyte flag sync:\tCannot be determined\n", ch);
}
} /* for ch */
break;
case CTC_8254:
printf("CTC is an 8254; all information is available\n\n");
port61 = inportb(0x61);
outportb(0x61, (port61 & 0xFC) | 0x01); /* Enable timer 2 gate */
for (ch = 0; ch < 3; ++ch) {
readback_channel(ch, &rbd[ch]);
lhf[ch] = infer_lobyte_hibyte_flag(ch);
}
printf("Parameter\t\tChannel 0\tChannel 1\tChannel 2\n\n");
printf("Access sequence:");
for (ch = 0; ch < 3; ++ch) {
switch (rbd[ch].status & 0x30) {
case 0x00:
printf("\tUninitialised");
rbd[ch].count = 0;
break;
case 0x10:
printf("\tLobyte only");
rbd[ch].count &= 0xFF;
break;
case 0x20:
printf("\tHibyte only");
rbd[ch].count &= 0xFF00;
break;
case 0x30:
printf("\tLobyte/hibyte");
} /* switch */
} /* for ch */
printf("\n");
printf("Operating mode:\t\t%d\t\t%d\t\t%d\n",
(rbd[0].status >> 1) & 0x07,
(rbd[1].status >> 1) & 0x07,
(rbd[2].status >> 1) & 0x07);
printf("BCD/binary mode:");
for (ch = 0; ch < 3; ++ch)
printf(rbd[ch].status & 1 ? "\tBCD\t" : "\tBinary\t");
printf("\n");
printf("Output pin state:");
for (ch = 0; ch < 3; ++ch)
printf(rbd[ch].status & 0x80 ? "\tHigh\t" : "\tLow\t");
printf("\n");
printf("Null Count flag:");
for (ch = 0; ch < 3; ++ch)
printf(rbd[ch].status & 0x40 ? "\tSet\t" : "\tClear\t");
printf("\n");
printf("Current raw count:\t0x%04X\t\t0x%04X\t\t0x%04X\n",
rbd[0].count, rbd[1].count, rbd[2].count);
printf("Lobyte/hibyte flag:");
for (ch = 0; ch < 3; ++ch) {
if ((rbd[ch].status & 0x30) == 0x30) {
switch (lhf[ch]) {
case LHF_INSYNC:
printf("\tCorrect\t");
break;
case LHF_OUTSYNC:
printf("\tReversed");
break;
default:
printf("\tUnknown\t");
}
}
else
printf("\tN/A\t");
} /* for */
printf("\n");
asm cli;
outportb(0x61, (inportb(0x61) & 0xFC) | (port61 & 0x03));
/* Restore timer 2 gate and speaker control bits */
asm sti;
break;
} /* switch ctctype */
exit(0);
}
-------------------------------- snip snip snip --------------------------------
## 7.28 CTC ACCESS UNDER OS/2
Native OS/2 applications do not need to access the CTC directly. This section
is concerned with DOS applications that can be run under OS/2 in a VDM (Virtual
DOS Machine).
The HW_TIMER option for the DOS session determines whether the DOS application
is given access to the real CTC, or whether it uses the virtual CTC driver,
VTIMER.SYS. Manipulating the CTC with the HW_TIMER option set ON may cause
interference with other DOS tasks, though I think it does not affect OS/2
because OS/2 uses the Real Time Clock for its timekeeping. I believe that OS/2
does not use CTC channel zero itself; it is only required for DOS tasks.
OS/2's VTIMER.SYS (virtual CTC emulator, used if HW_TIMER is OFF) is rather
interesting. I have paraphrased some information from the OS/2 red book (OS/2
Version 2.0 Volume 2: DOS and Windows Environment), if anyone has more detailed
or newer information I'd like to see it. (*)
## 7.28.1 OS/2 VTIMER.SYS: CTC CHANNEL ZERO
VTIMER.SYS is able to generate virtual (emulated) interrupts at 54.9254 ms
intervals, or 13.7314 ms intervals (four times faster). If the DOS session
reprograms the divisor of channel zero, it gets its ticks at 13.7314 ms
intervals, regardless of the actual divisor value it programmed (presumably
unless it programmed a divisor of 65536). VTIMER.SYS runs the real CTC channel
zero at 54.9254 ms, or 13.7314 ms if one or more DOS sessions are running at
this rate. The 13.7314 ms interrupt capability is required for GW-BASIC which
uses a four times faster interrupt for its PLAY command (music).
Latching channel zero causes a "random value derived from the system time" to be
loaded into the emulated Latch register, which can then be read. This design
decision was based on the fact that the count register is often used to provide
a random number seed, and this approach supposedly gives the DOS application a
"sense of elapsed time". The documentation does not say whether read-back is
supported, but I suspect it is not.
In other words, it is not possible to use channel zero for timing in a DOS
session under OS/2, unless HW_TIMER is set to ON. Stick to the tick count
variable and/or timer interrupts, at the normal rate, if you want your program
to run properly under OS/2.
## 7.28.2 OS/2 VTIMER.SYS: CTC CHANNEL ONE
Apparently this channel only supports read accesses, which presumably return a
random number or a number derived from the system time. All other accesses are
ignored. At a guess, I would say that each read of the data register will yield
a random or time-derived value.
## 7.28.3 OS/2 VTIMER.SYS: CTC CHANNEL TWO
This is interesting. Channel two is linked up with the emulated Port B (see
section »» 7.5) and OS/2 "serialises" speaker access from different tasks.
When we generate a speaker tone, we program the divisor into channel two (which
will determine the tone frequency), and then enable the speaker by means of the
bottom two bits in Port B. VTIMER.SYS remembers the divisor value, and when
the bits are set in Port B, it calls the OS/2 "kernel beep", which may block if
it is already beeping on behalf of a different process. After completion of any
beep in progress, the kernel beep function programs the correct value into the
real channel two, and programs the real Port B to start the beep. When the DOS
session turns off the bits in Port B, the beep is stopped and the kernel beep
becomes available for use by other sessions.
The "serialisation" can be pre-empted by an "interrupt time beep service" which
is somehow used if the beep is issued by the keyboard scancode interrupt
handler, to support the "keyboard buffer full" beep issued by the BIOS in a
DOS session. Interesting!
## 7.29 GENERATING AUDIO TONES ON THE SPEAKER
Although this is unrelated to timing...
The PC speaker interface circuitry is thoroughly documented in section »» 7.5.
CTC channel two is used for generating audio. It is normally operated in mode
three, to produce a square wave signal. It can be used in different ways,
though - see section »» 10.7.1 for the PWM audio generation technique.
The speaker interface is controlled by two bits in the read/write register
at I/O address 61 hex:
7 6 5 4 3 2 1 0
* * * * * * . . Not applicable to speaker control - do not modify!
. . . . . . * . Speaker data
. . . . . . . * Timer 2 Gate
The Timer 2 Gate signal is directly connected to the 'gate' input of timer
channel two. This signal must be high in order for the counter to decrement.
When the gate signal goes low, the timer output goes high immediately, and
counting ceases. The count register is reloaded from the divisor register on
the next 1.193182 MHz clock pulse, and when the gate input goes high again,
counting resumes starting at the divisor value, thus synchronising the counter.
The Speaker data output is logical-ANDed with the output from timer two, to
drive the speaker. Thus, to generate a tone using timer 2, Speaker data should
be set to '1' and Timer 2 gate should also be set to '1'. The frequency of the
tone will be 1193181.6666... divided by the divisor value programmed into CTC
channel two.
To generate audio by bit manipulation, Timer 2 gate should be set to zero.
This disables timer two and forces its output high. The speaker can then be
directly controlled via Speaker data. Setting this bit high allows current
to flow in the speaker coil, causing the cone to move outwards (or inwards,
depending on which way the speaker is wired - it doesn't matter really).
Setting the bit low causes the cone to return to its normal position.
Toggling the bit at rate of n toggles per second gives a frequency of n/2 Hz.
## 7.30 SAMPLE PROGRAM: GENERATING A TONE USING CTC CHANNEL TWO
The following program generates a tone at approximately 1KHz for approximately
one second, using CTC channel two.
See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-------------------------------- snip snip snip --------------------------------
/*
Sample program #8
Demonstrates generating a tone using timer channel two
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save this file to SAMPLE8.C and compile with:
bcc -I<inc_path> -L<lib_path> -ms sample8.c
Where inc_path is the path to your C header files and your startup modules
C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, and cli */
#include <stdio.h> /* Needed for printf() */
#include <stdlib.h> /* Needed for exit() */
#define FALSE 0
#define TRUE 1
#define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL)
#define DIVISOR(frequency) ((unsigned int) ((1193181.6666 / frequency) + 0.5))
#define FREQUENCY 1000 /* Tone frequency in Hz */
unsigned long read_bios_tick_count(void) {
unsigned long ct;
asm pushf;
asm cli;
ct = * BIOS_TICK_COUNT_P;
asm popf;
return ct;
}
int has_tick_occurred(void) {
static unsigned long old_tick_count = 0xFFFFFFFFL;
if (read_bios_tick_count() != old_tick_count) {
old_tick_count = read_bios_tick_count();
return TRUE;
}
return FALSE;
}
void init_channel(unsigned int channum, unsigned int accessmode,
unsigned int mode, unsigned int reload) {
if (channum > 2 || accessmode < 1 || accessmode > 3)
return;
asm pushf;
asm cli;
outportb(0x43, (channum << 6) + (accessmode << 4) + ((mode & 0x07) << 1)); /* Mode */
outportb(0x40 + channum, reload & 0xFF); /* Reload reg lobyte */
outportb(0x40 + channum, (reload >> 8) & 0xFF); /* Reload reg hibyte */
asm popf;
return;
}
void turn_tone_on(unsigned int divisor) {
init_channel(2, 3, 3, divisor); /* Channel 2, 16-bit, mode 3 */
asm pushf; /* Preserve interrupt flag */
asm cli;
outportb(0x61, inportb(0x61) | 0x03); /* Enable timer and speaker */
asm popf; /* Restore interrupt flag */
return;
}
void turn_tone_off(void) {
asm pushf; /* Preserve interrupt flag */
asm cli;
outportb(0x61, inportb(0x61) & 0xFC); /* Disable speaker */
asm popf; /* Restore interrupt flag */
return;
}
void main(void) {
unsigned int n = 0;
printf("Sample program #8 - Demonstrates generating a tone using CTC channel two\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
printf("Tone frequency is %d Hz\n", FREQUENCY);
has_tick_occurred(); /* Init has_tick_occurred() */
while (has_tick_occurred() == FALSE)
; /* Wait for a tick to occur */
turn_tone_on(DIVISOR(FREQUENCY));
while (n < 18) /* Stop after one second */
if (has_tick_occurred())
++n;
turn_tone_off();
exit(0);
}
-------------------------------- snip snip snip --------------------------------
## 7.31 TIMING SHORT PERIODS USING CTC CHANNEL TWO
{JAM} The ideas and code example in this section are largely from Prof.
John Mertus's document.
Reading a CTC channel requires three I/O accesses, and I/O accesses are
notoriously slow by comparison to memory accesses, particularly on fast
machines. Referring to section »» 7.5, CTC channel two's output is readable
on bit 5 of Port C at I/O address 62h (PC and XT) or bit 5 of Port B at I/O
address 61h (AT and later), and this port can be read in a single I/O access.
This can be useful when a short (microsecond-level) delay must be timed
especially accurately.
With this approach, CTC channel two is used in mode zero (see section »» 7.8.2),
the 'interrupt on terminal count' mode. In this mode, we can write a count
value to the CTC, then watch the Timer 2 Output signal and wait for it to go
high, signalling that the time period has expired.
When using this technique, you must ensure that the Timer 2 Gate output on
bit 0 of Port B at I/O address 61h is high (CTC channel two will not count
if this signal is low) and that the Speaker Data signal on bit 1 of the same
register is low, to avoid sending horrible noises to the speaker!
The following code fragment shows how to produce a short (five microseconds
plus overhead) pulse on the strobe output of a parallel port, using this
approach. It will not work on the old PC and XT - see the comment by the
WaitCTC: label.
See section »» 6.22 for the explanation of the pushf/cli/popf technique.
You could also time longer periods, up to 54.9 ms, but in this case you would
remove the PUSHF/CLI and STI around the delay code and set and clear the bit
in the parallel port register in a different way - to clear it, use PUSHF/CLI,
read the port, AND the value, write it back, and POPF, and same to set the bit
(but use OR instead of AND, of course). This leaves the body of the delay
loop operating with interrupts enabled, which is desirable to avoid problems
with interrupt latency, etc, but could cause problems for example if the
keyboard buffer filled up and a beep was issued, because this would result
in Port B and CTC channel 2 being reprogrammed part-way through the delay loop.
A pop-up TSR could also issue a beep, causing the same problem. This would
require intercepting int 10h (BIOS video output, generates a beep) and int 9
(keystroke interrupt) or int 15h keystroke intercept, and even then, this would
not prevent some interrupt-triggered code from reprogramming CTC channel 2.
In other words, I can't see any safe way to implement longer delays with
interrupts enabled using this technique (except in a controlled environment).
-------------------------------- snip snip snip --------------------------------
NCTCClocks EQU 6 ; Six CTC clocks (5us) for the delay
LPTPortBase DW 3BCh ; Set to your LPT port base address
; Somewhere in the initialisation code, set up Timer 2 Gate and Speaker Data:
pushf
cli ; No interrupts
in al,61h ; Get Port B
and al,11111101b ; Turn off Speaker Data
or al,00000001b ; Turn on Timer 2 Gate
out 61h,al ; Write it back
popf ; Restore interrupt flag
; ...
; Then produce the short pulse:
mov dx,LPTPortBase ; Get parallel port base I/O address
inc dx
inc dx ; Point to control register
mov al,090h ; Timer 2, lobyte only, mode 0, binary
pushf
cli ; No interrupts
out 43h,al ; Send command byte - prepare the CTC
in al,dx ; Get parallel port value
and al,11111110b ; Clear bit 0 (set pin 1, -STROBE, high)
mov ah,al ; To AH for later
inc ax ; Set bit 0 (set pin 1, -STROBE, low)
out dx,al ; Set the I/O register
; At this point the -STROBE pin goes low
mov al,NCTCClocks ; Number of CTC clocks to wait
out 42h,al ; Start the timer
in al,61h ;!! Use 62h for PC and XT!!
WaitCTC: in al,61h ;!! Use 62h for PC and XT!!
test al,20h ; Test bit 5 - has the time expired?
jz WaitCTC ; If not, loop
mov al,ah ; Get value with bit 0 off
out dx,al ; Write it
; At this point the -STROBE pin returns high
popf ; Restore interrupt flag
-------------------------------- snip snip snip --------------------------------
Note that CTC channel two is being used in lobyte-only mode for maximum access
speed; if you need to delay more than 255 CTC clocks, use the timer in the
lobyte-hibyte mode and write a two-byte reload register value.
You would want to avoid using CTC channel 2 for audio generation, including
the standard BIOS beep, if you were using this technique inside an interrupt
handler, because any beep in progress will be cut off when this code executes.
{JAM} says: "On reasonably fast machines, timer 2 can be used to create delays
from 5 to 54,000 microseconds with 1 to 2 microsecond accuracy".
## 7.32 TIMING SHORT PERIODS USING MODE THREE
For timing short periods, where an absolute timestamp is not required, a
simplified technique can be used, using CTC channel zero in mode three.
Traditionally the BIOS programmed CTC channel zero to operate in mode three
with a Reload value of zero. Modern BIOSes seem to prefer to use mode two.
See section »» 7.4.2 for details.
Referring to sections »» 7.8.5, »» 7.15.2 and »» 7.20, in mode three, the raw
value read from the count register decrements in steps of two, each step
corresponding to one 0.8381 us CTC clock period. Therefore, periods of time
comfortably less than about 27 ms can be measured by reading the counter,
storing the value read, then repeatedly reading the counter, calculating the
difference, and waiting for this difference to exceed the desired number, which
will be twice the number of 0.8381 us periods (because the timer decrements in
steps of two).
This technique is demonstrated in the sample program in section »» 7.34.
## 7.33 VERTICAL RETRACE
A video monitor display is created by the electron beam in the monitor (colour
monitors have three) which scans the screen in a 'raster' fashion similar to
the way you read a book (though a lot quicker :-) The beam starts at the top
left corner and draws one line from left to right, then returns to the left and
scans the line below, and so on until the entire screen has been scanned. Each
of these horizontal scans is called a line, and at least 15000 lines are scanned
each second (depending on the screen resolution and timing parameters).
Each time the electron beam reaches the right side of the screen, it returns
very quickly to the left side of the screen, ready to scan the next line. This
short 'return' time is called the horizontal retrace. The horizontal retrace
interval is very short (a few microseconds) and is significant on some old CGA
cards because a 'snow' effect was produced unless the video buffer was only
accessed during the horizontal retrace interval.
After the full screen has been scanned, the beam turns off and returns to the
top of the screen. This is the vertical retrace interval, which occupies a
length of time in the order of one or two milliseconds (depends on video mode
timing parameters), and occurs about 50 to 70 times per second (equal to the
field rate, or vertical scan rate, sometimes called the 'refresh' rate).
LCD displays are not physically scanned in the same way, but they usually get
their display information from a signal which is raster oriented. In any case,
vertical retrace is emulated on LCD machines, for compatibility.
Vertical retrace is indicated by a status bit in the video status register at
I/O location 3BA hex (MDA, Hercules, and EGA and VGA in monochrome modes) or
3DA hex (CGA, MCGA, and EGA and VGA in colour modes).
For CGA, MCGA, EGA, and VGA cards, bit 3 indicates vertical retrace, and is
set during the retrace interval (i.e. clear during the display period) except
for the MCGA card in 640x480 monochrome mode, when the bit has the opposite
polarity (although the status register appears at 3DA, the colour address!).
The MDA card does not have a vertical retrace indication, though the Hercules
card does indicate vertical sync on bit 7 of the register at 3BA, with opposite
polarity, i.e. the bit is clear during retrace).
Some video cards are also able to generate IRQ2/9 on vertical retrace but
standard VGA cards do not have this facility, so I will not describe it here.
This interrupt can be simulated fairly successfully using CTC channel 0. This
technique is described in section »» 10.16.
The word at low memory address 0040:0063 (or 0000:0463) contains the I/O
address of the CRTC which can be used to determine whether the video system
is colour or monochrome. A value of 3B4 hex indicates monochrome. In this
case vertical retrace detection is unreliable, as the MDA does not have any
vertical retrace indication. A value of 3D4 hex indicates colour, in which
case vertical retrace is indicated by bit 3 in the register at I/O address
3DA hex.
## 7.34 SAMPLE PROGRAM: TIMING SHORT PERIODS USING MODE THREE
The following program uses CTC channel 0 in mode three to measure short
durations to provide a striped background colour on a VGA adapter. It uses
the VGA vertical retrace signal to synchronise the time periods with the
start of each screen update.
The effect of turning the computer's turbo switch on and off is minimal, and
is not cumulative; this demonstrates that the program is correctly using the
CTC to measure the time delay.
See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-------------------------------- snip snip snip --------------------------------
/*
Sample program #9
Demonstrates timing short periods using mode three
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save this file to SAMPLE9.C and compile with:
bcc -I<inc_path> -L<lib_path> -ms sample9.c
Where inc_path is the path to your C header files and your startup modules
C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, and cli */
#include <bios.h> /* Needed for bioskey() */
#include <dos.h> /* Needed for MK_FP() */
#include <stdio.h> /* Needed for printf() */
#include <stdlib.h> /* Needed for exit() */
#define FALSE 0
#define TRUE 1
#define DELAY 1790 /* Delay 1790 x 0.8381 us = 1.5 ms */
#define DAC_ADDR 0x3C8 /* VGA DAC address register */
#define DAC_DATA 0x3C9 /* VGA DAC data register (write) */
#define BIOSSHIFT (*((unsigned char far *)MK_FP(0x40, 0x17)))
unsigned int read_timer0_mode3_raw(void) {
asm pushf;
asm xor al,al;
asm cli;
asm out 43h,al;
asm in al,40h;
asm mov ah,al
asm in al,40h
asm popf;
asm xchg al,ah;
return _AX; /* Return raw value */
}
void main(void) {
unsigned int video_status; /* I/O address of video status reg */
unsigned int colour[3]; /* Background colour - R, G, and B */
unsigned int rgbsel; /* Selects which colour to change */
unsigned int ctcval, newctc; /* Raw mode three values */
printf("Sample program #9 - Demonstrates timing short periods using mode three\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
printf("Press the Ctrl key to exit\n");
video_status = *((unsigned int far *)MK_FP(0x40, 0x63)) + 6;
/* First, make sure it's in mode three! */
asm cli;
outportb(0x43, 0x36);
outportb(0x40, 0);
outportb(0x40, 0);
asm sti;
/* Wait for vertical retrace to start */
while ((inportb(video_status) & 0x08) == 0)
;
/* Start of retrace - reset colours */
newscr:
colour[0] = colour[1] = colour[2] = 0;
rgbsel = 0;
asm cli;
outportb(DAC_ADDR, 0);
outportb(DAC_DATA, 0);
outportb(DAC_DATA, 0);
outportb(DAC_DATA, 0);
asm sti;
/* Check for CTRL pressed and terminate if so */
if (BIOSSHIFT & 0x04) {
while (bioskey(1))
bioskey(0); /* Flush buffer */
exit(0);
}
/* Wait for start of display */
while ((inportb(video_status) & 0x08) != 0)
;
/* Get the time now */
ctcval = read_timer0_mode3_raw();
/* Loop waiting for nominated time period to elapse, check for end of display */
while (1) {
do {
if ((inportb(video_status) & 0x08) != 0)
goto newscr; /* If retrace has started */
newctc = read_timer0_mode3_raw(); /* Sample the time */
newctc = ctcval - newctc; /* Get CTC clocks elapsed x2 */
}
while (newctc < (DELAY * 2)); /* Loop until desired time */
/* Time has elapsed - bump time reference and change the background colour */
ctcval -= (DELAY * 2); /* Use * 2 because of mode 3 */
colour[rgbsel] += 22; /* Increase R/G/B component */
if (++rgbsel > 2) /* Change R/G/B selector */
rgbsel = 0;
asm cli;
outportb(DAC_ADDR, 0);
outportb(DAC_DATA, colour[0]);
outportb(DAC_DATA, colour[1]);
outportb(DAC_DATA, colour[2]);
asm sti;
} /* for */
} /* main() */
-------------------------------- snip snip snip --------------------------------
## 7.35 THE REAL TIME CLOCK (RTC)
The RTC/RAM chip is a Motorola MC146818A chip or workalike. It is not present
in the original PC and XT and may not be present in non-hardware-compatible
machines. It is often implemented as part of an ASIC, or in a hybrid module
such as the DS1287, which contains the RTC/RAM chip, crystal, and backup
battery.
It is a CMOS device, containing a crystal oscillator and divider with interrupt
and alarm logic, a non-volatile CMOS RAM array which stores the BIOS parameter
settings, and a processor interface based on the CMOS RAM register file (which
contains 64 or sometimes 128 registers).
The crystal oscillator normally operates at 32768 Hz, using a small watch type
crystal. The RTC has an interrupt output, which is wired to IRQ8 (normally
mapped to int 70h). The RTC is accessed at I/O addresses 70 hex and 71 hex.
Both ports are 8-bit and should only be accessed using 8-bit I/O instructions.
The port at 70h is the address select port, which selects which of the 64 or
128 internal registers will be addressed by an access to I/O address 71h.
The original MC146818 chip is bus-addressable, and this address/data system
may be implemented in logic on the motherboard, not on the RTC/RAM chip itself.
After the register has been specified by writing a register number to port 70h,
the selected register's contents can then be read or written via the port at
I/O address 71 hex. This address register and data register technique reduces
the amount of I/O space required by the RTC, and is not actually part of the
MC146818, but is implemented on the motherboard or in the ASIC. The same
technique is used in the CRT Controller chip and other chips on video cards.
Always disable interrupts around the access sequence, otherwise an interrupt
routine could select a different RTC register, causing your code to read or
write the wrong register. Also, note that the address select register at I/O
address 70h is write-only. Reading the register will yield an undefined value.
## 7.35.1 READING AND WRITING RTC REGISTERS
Here are functions to read and write RTC registers. Inline assembler is
required for pushf and popf. See section »» 6.22 for the explanation of
the pushf/cli/popf technique. The cli could be replaced by the disable()
pseudofunction. Change inportb() and outportb() to inp() and outp() for
Microsoft C, I think.
-------------------------------- snip snip snip --------------------------------
unsigned char read_rtc_register(unsigned char reg_num) {
unsigned char rv;
asm pushf;
asm cli;
outportb(0x70, reg_num);
asm jmp SHORT $+2
asm jmp SHORT $+2
asm jmp SHORT $+2
rv = inportb(0x71);
asm popf;
return rv;
}
void write_rtc_register(unsigned char reg_num, unsigned char value) {
asm pushf;
asm cli;
outportb(0x70, reg_num);
asm jmp SHORT $+2
asm jmp SHORT $+2
asm jmp SHORT $+2
outportb(0x71, value);
asm popf;
return;
}
-------------------------------- snip snip snip --------------------------------
## 7.35.2 ALLOCATION OF THE RTC REGISTERS
The first 10 registers (registers 0 to 9) are the date and time registers
(including the alarm settings). These registers cannot be accessed during the
update period, which is approximately two milliseconds long and occurs every
second (details are given later). Registers 10 to 13 are control registers.
The remaining registers (14-63 on a standard MC146818 which has 64 registers,
or 14-127 on an enhanced version) are general purpose CMOS RAM locations, which
are used by the BIOS to store setup information, and do not relate to timing.
The time and date values are configurable for either packed BCD or binary data
format, but the BIOS uses the packed BCD format, and some workalike chips do
not support binary format, so for practical purposes, packed BCD format is
mandatory. See the glossary for a description of packed BCD.
Important! The date and time registers (registers 0 to 9) will yield correct
values only if no update is in progress. See notes on Register A for details.
These registers should not be written unless the 'Set' bit in Register B is
set. See notes on Register B for details.
The registers are as follows:
Reg Function Format Range
--- -------- ------ -----
0 Seconds Two digit packed BCD 0 to 59
1 Seconds alarm Two digit packed BCD 0 to 59
2 Minutes Two digit packed BCD 0 to 59
3 Minutes alarm Two digit packed BCD 0 to 59
4 Hours See below
5 Hours alarm See below
6 Day of week BCD 1 to 7 (see below)
7 Date of month Two digit packed BCD 1 to 31
8 Month Two digit packed BCD 1 to 12
9 Year Two digit packed BCD 0 to 99
10 Register A See below
11 Register B See below
12 Register C See below
13 Register D See below
The hours and hours alarm registers (registers 4 and 5) are formatted in
12-hour or 24-hour mode, depending on the setting of bit 1 of Register B
(see the description for this bit). In 12-hour mode, bits 6-0 of the
hours registers are the hours value, in the range 1 to 12, and bit 7 is
the PM indicator (set indicates PM). In 24-hour mode, bits 7-0 of the
hours registers are in 24-hour format (range 0 to 23).
The seconds alarm, minutes alarm, and hours alarm registers may be set to a
value from 0C0 hex to 0FF hex to indicate 'don't care'. For example if the
seconds alarm value is zero, the minutes alarm value is 30 (stored in packed
BCD form, of course), and the hours alarm value is 0FF hex, the alarm will
be signalled at half past every hour. Note that this 'don't care' function
may not be implemented in all ASIC workalikes.
The day of week register (register 6) simply counts 1, 2, 3, 4, 5, 6, 7, 1,
2... where 1 means Sunday, 2 means Monday, etc. The RTC does not calculate
the day of the week from the date. This register must be set by software.
It is not used by the BIOS RTC functions or by DOS and will not necessarily
be set correctly. Software normally calculates the day of week from the other
date information rather than using this register. The RTC uses this register
to switch between standard time and daylight saving time if daylight saving is
enabled, but the daylight saving function is not used in PCs so there is no
need to make sure that this register is set correctly.
## 7.35.3 RTC REGISTER A
Register A is register number 10. It is read/write except bit 7, which is
read-only:
7 6 5 4 3 2 1 0
* . . . . . . . Update In Progress (UIP) flag
. * * * . . . . Prescaler control bits
. . . . * * * * Periodic Interrupt rate control
The UIP flag, if set, indicates that an update is in progress or is imminent.
An update occurs once every second and takes approximately two milliseconds.
During the update period, the values read from the date and time registers
(though not the alarm registers) are changing and are not valid, because the
RTC chip operates quite slowly internally (being low power CMOS) and it takes
a while for an update to 'ripple through' from the seconds register all the
way up to the year register.
If the UIP flag is set, the date and time registers (registers 0-9) should not
be accessed. Software must wait until the UIP flag becomes clear before reading
any time or date related registers. The UIP flag becomes active approximately
244 us prior to the start of the update cycle, therefore the read or write
operation must take less than 244 us to ensure that it completes before the
update cycle begins.
The Prescaler control bits determine what crystal frequency the RTC expects,
and allow the prescaler and divider to be held reset. The values are:
bit 6 5 4
0 0 0 Operation with 4.194304 MHz crystal
0 0 1 Operation with 1.048576 MHz crystal
0 1 0 Operation with 32768 Hz crystal (default)
0 1 1 Undefined
1 0 x Undefined
1 1 x Hold prescaler and divider reset (stops counting)
(x means don't-care)
While the prescaler and divider are held reset, counting and updating ceases.
The first update will occur half a second after this condition is removed.
The Periodic Interrupt rate control bits determine the periodic interrupt
rate (d'oh :-) Here are the values:
bit 3 2 1 0 Period Ints per second
0 0 0 0 No periodic interrupt
0 0 0 1 3.90625 ms 256 (see note below)
0 0 1 0 7.8125 ms 128 (see note below)
0 0 1 1 122.0703125 us 8192
0 1 0 0 244.140625 us 4096
0 1 0 1 488.28125 us 2048
0 1 1 0 976.5625 us 1024 (BIOS default)
0 1 1 1 1.1953125 ms 512
1 0 0 0 3.90625 ms 256
1 0 0 1 7.8125 ms 128
1 0 1 0 15.625 ms 64
1 0 1 1 31.25 ms 32
1 1 0 0 62.5 ms 16
1 1 0 1 125 ms 8
1 1 1 0 250 ms 4
1 1 1 1 500 ms 2
Note: Combinations 0001 and 0010 duplicate 1000 and 1001 respectively.
If the RTC is operating from a 1MHz or 4MHz crystal (prescaler control bits
are 00x), combinations 0001 and 0010 give interrupt rates of 30.517578125 us
(32768 interrupts per second) and 61.03515625 us (16384 interrupts per second)
respectively. 1MHz and 4MHz crystals are not used with RTCs in PCs because
of the increased power consumption.
## 7.35.4 RTC REGISTER B
Register B is register number 11. It is fully read/write:
7 6 5 4 3 2 1 0
* . . . . . . . Set flag (1 = set mode)
. * . . . . . . PIE, Periodic Interrupt Enable (1 = enable)
. . * . . . . . AIE, Alarm Interrupt Enable (1 = enable)
. . . * . . . . UIE, Update Interrupt Enable (1 = enable)
. . . . * . . . SQWE, Square wave enable, not used in PCs
. . . . . * . . DM, BCD/binary mode (1 = binary)
. . . . . . * . 12/24-hour mode (0 = 12-hour, 1 = 24-hour)
. . . . . . . * DSE, Daylight Saving Enable (1 = enable)
The Set flag must be set by software before any real-time registers (current
date and time) are modified. When the bit is set, any real-time register
update in progress is aborted, and while the bit is set, updates are prevented
and the UIP bit in Register A remains clear. After setting the real-time
registers, the SET bit must be cleared to resume normal operation.
The PIE, AIE, and UIE enable the periodic, alarm, and update interrupts
respectively, if set. The periodic interrupt occurs regularly as defined by
bits 0-3 of Register A. The update interrupt, if enabled, occurs every second,
immediately following an update. The alarm interrupt occurs whenever the
hours, minutes and seconds registers match the time programmed into the alarm
registers. See the note after the register list for alarm register details.
The SQWE bit enables the square wave output at the frequency set by bits 0-3 of
Register A. This pin is not used in PC applications.
If the 12/24-hour mode is changed, the hours register should be reprogrammed.
Daylight Saving mode, if enabled, causes the time to jump forward from 01:59:59
to 03:00:00 on the morning of the last Sunday in April, and backward from
01:59:59 to 01:00:00 on the last Sunday in October. The day of week register
must be set correctly for this to work properly. The PC does not use this
function.
## 7.35.5 RTC REGISTER C
Register C is register number 12. It is read-only; writes are ignored.
It contains three interrupt source flags and the combined interrupt flag.
7 6 5 4 3 2 1 0
* . . . . . . . IRQF (combined interrupt flag)
. * . . . . . . PF (periodic flag)
. . * . . . . . AF (alarm flag)
. . . * . . . . UF (update flag)
. . . . * * * * Unused; zero, read-only
The three interrupt source flags are set if the condition that would generate
the interrupt has occurred, regardless of whether the interrupt source is
enabled (via Register B). These can be used to permit software polling of
these conditions, if generating an actual interrupt is not justified.
Any active interrupt source flags are cleared immediately after reading this
register; thus if several interrupt sources are active, the software must be
careful to check for each possible interrupt flag after every read of this
register, otherwise a signal may be missed.
The IRQF flag (combined interrupt flag) is set if the interrupt output from
the RTC chip is active. This will be true if any of the interrupt source
flags in this register are set in conjunction with that interrupt source being
enabled via Register B.
## 7.35.6 RTC REGISTER D
Register D is register number 13. Only bit 7 is meaningful, and this bit is
read-only.
7 6 5 4 3 2 1 0
* . . . . . . . VRT, Valid Ram and Time flag
. * * * * * * * Unused; zero, read-only
The VRT flag indicates whether a power-up has occurred. It is cleared during
loss of supply voltage, and is set immediately after a read of Register D.
## 7.35.7 READING THE RTC
When reading the RTC's real-time registers it is necessary to avoid reading
them during the update period, during which time they cannot be accessed by
the processor (reading registers will yield undefined values, and writes will
be ignored). Registers A, B, C, and D can be accessed at any time.
Your software can use the UIP flag bit in Register A to determine whether an
update is in progress or imminent. If this flag is clear, your software then
has up to 244 us in which to perform the desired register access(es), and may
then re-check the UIP flag and make more accesses if appropriate.
If the UIP flag is set, the software may have to wait up to approximately 2.25
ms before the UIP flag is clear. If such a long delay in a read-RTC function
is undesirable, a possible solution in some cases could be to store the time
each time the RTC is read, and if the RTC is not available due to an update
cycle being in progress, return the most recently read RTC value instead.
Alternatively, the Update Interrupt and/or the Update Flag in Register C can
be used to schedule reads of the RTC so they occur immediately after an update,
either under interrupt (if the RTC interrupt is not required for any other
purpose), or by polling the Update Flag and reading the real-time registers
as soon as the flag reads as '1' (assuming no long background processes are
active, this gives the code almost a whole second to make its RTC accesses
before the next update cycle will begin.
## 7.35.8 SAMPLE PROGRAM: A TSR CLOCK USING INT 8 AND THE RTC
This program is a TSR which hooks interrupt 8 and uses the RTC to display a
persistent HH:MM:SS format time in the top right corner of the screen.
-------------------------------- snip snip snip --------------------------------
NAME SAMPLE10
; Sample program #10
; Demonstrates a TSR clock using int 8 and reading the RTC directly
; Part of the PC Timing FAQ / Application notes
; By K. Heidenstrom (kheidens@actrix.gen.nz)
;
; This program assembles into SAMPLE10.COM, a TSR program which displays the
; current time in the top right corner of the screen in text modes. It uses
; int 8 to get execution 18.2065 times per second, reads the RTC time directly
; from the RTC chip, and updates the screen every second. This program does
; not attempt to ascertain that an RTC is present. Also, it has no uninstall
; facility.
;
; If a non-standard video mode (i.e. mode 14 hex or higher) is in use, this
; program will assume that it is a text mode. This will probably result in
; disturbance to the display in high resolution graphics modes. This program
; is intended to be instructional only.
;
; Save this file to SAMPLE10.ASM and assemble with:
; masm SAMPLE10;
; link SAMPLE10;
; exe2bin SAMPLE10.exe SAMPLE10.com
; or
; tasm SAMPLE10;
; tlink /t SAMPLE10;
;
ComFile SEGMENT
ASSUME cs:ComFile,ds:ComFile,es:nothing,ss:nothing
ORG 100h ; COM-type file
Main PROC near
jmp Main2
Main ENDP
Hours DB 0FFh ; Hours (BCD) of last update
Minutes DB 0FFh ; Minutes (BCD) of last update
Seconds DB 0FFh ; Seconds (BCD) of last update
ASSUME ds:nothing
; The following function handles int 8, the timer tick interrupt. It reads
; Register A and checks for an update in progress, and skips if so. It
; then reads the seconds register and checks to see whether he seconds have
; changed. If they have, it reads the hours and minutes registers also, and
; then displays the current time in HH:MM:SS format in the top right hand
; corner of the screen if the screen is currently in text mode.
;
; This procedure calls no DOS or BIOS functions.
;
; Note that the whole routine, and also the DisplayTime subroutine which is
; called by this routine, and its subroutines, run with interrupts disabled.
NewInt08 PROC far ; Int 8 intercepter
pushf ; Preserve flags
push ax ; Keep register
cli ; Just in case
mov al,0Ah ; Register number for register A
out 70h,al ; Set it
jmp SHORT $+2 ; Delay
in al,71h ; Read register
and al,80h ; Test update-in-progress flag
jnz Chain08 ; If busy, do it next time
jmp SHORT $+2 ; Delay
out 70h,al ; Select register zero - seconds
jmp SHORT $+2 ; Delay
in al,71h ; Read seconds
cmp al,Seconds ; Did seconds change?
je Chain08 ; If not, do nothing on this interrupt
mov Seconds,al ; Store new seconds
mov al,2 ; Minutes register
jmp SHORT $+2 ; Delay
out 70h,al ; Set it
jmp SHORT $+2 ; Delay
in al,71h ; Get minutes
mov Minutes,al ; Store
mov al,4 ; Hours register
jmp SHORT $+2 ; Delay
out 70h,al ; Set it
jmp SHORT $+2 ; Delay
in al,71h ; Get hours
mov Hours,al ; Store
call DisplayTime ; Display the time
Chain08: pop ax ; Restore
popf ; Restore
DB 0EAh ; JMP xxxx:yyyy
Old08Ofs DW 0 ; Vector to original handler - Offset
Old08Seg DW 0 ; Segment
NewInt08 ENDP
; This procedure uses the time values stored in Hours, Minutes, and Seconds to
; create a time value in the form HH:MM:SS and stores this to the top right
; corner of the screen. It checks the current video mode to avoid overwriting
; video memory incorrectly when a graphics mode is active, and also supports
; non-standard screen resolutions. It does not support Hercules graphics mode,
; because there is no standard video mode number for this mode as it is not
; officially recognised by IBM.
DisplayTime PROC near ; Display the current time
push ds ; Need DS
xor ax,ax ; Zero
mov ds,ax ; Address BIOS vars using DS
mov al,ds:[449h] ; Get video mode
cmp al,4 ; Check for modes 0-3 (text)
jb TextMode ; If so
cmp al,7 ; Check for MDA text mode
je TextMode ; If so
cmp al,14h ; Check for last standard graphics mode
jb GraphMode ; If graphics, otherwise assume text
TextMode: push bx ; Keep BX
mov al,ds:[484h] ; Get number of lines minus one
inc ax ; Get number of lines (not minus one)
mov ah,ds:[462h] ; Get active page
mul ah ; Get starting line number
add ax,ds:[44Ah] ; Add number of columns
shl ax,1 ; Get offset of end of line
sub ax,18 ; Back up to 9 chars back from end
xchg ax,bx ; To BX
cmp BYTE PTR ds:[463h],0B4h ; Check for monochrome
mov ax,0B000h ; Prepare for monochrome
je HaveRegen ; If so
mov ah,0B8h ; If not, use CGA regen buffer
HaveRegen: mov ds,ax ; Address regen buffer with DS
mov WORD PTR ds:[bx-2],720h ; Space before time
mov al,Hours ; Get hours
and al,7Fh ; Mask off AM/PM bit
call StoreBCD ; Convert BCD to ASCII and store
mov al,Minutes ; Get minutes
call StoreColonBCD ; Convert BCD to ASCII and store
mov al,Seconds ; Get seconds
call StoreColonBCD ; Convert BCD to ASCII and store
mov WORD PTR ds:[bx],720h ; Space after time
pop bx ; Restore BX
GraphMode: pop ds ; Fix up
ret ; Done
DisplayTime ENDP
StoreColonBCD PROC near
mov WORD PTR ds:[bx],":"+700h ; Colon (with attribute)
inc bx
inc bx ; Bump pointer
StoreBCD PROC near
push ax ; Keep
shr al,1
shr al,1
shr al,1
shr al,1
call StoreBCDChar
pop ax
and al,0Fh
StoreBCDChar PROC near
add al,"0"
mov ah,7
mov ds:[bx],ax
inc bx
inc bx ; Bump pointer
ret
StoreBCDChar ENDP
StoreBCD ENDP
StoreColonBCD ENDP
Discard EQU $ ; Discard point
TSRParas = (OFFSET (Discard-@curseg+15) SHR 4)
ASSUME ds:ComFile
SignOnMsg DB "Sample program #10 - TSR clock using int 8 and direct RTC access",13,10
DB "Part of the PC Timing FAQ / Application notes",13,10
DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10
DB "Installed",13,10,13,10,"$"
; Check DOS version
Main2 PROC near
mov ah,30h
int 21h
cmp al,2 ; Expect DOS 2.0 or later
jae DOS_Ok
int 20h
; Intercept int 8
DOS_Ok: mov ax,3508h
int 21h ; Get vector for int 8
mov [Old08Ofs],bx
mov [Old08Seg],es ; Store it
mov dx,OFFSET NewInt08
mov ax,2508h
int 21h ; Set new vector
mov es,ds:[2Ch] ; Get segment of environment block
mov ah,49h
int 21h ; Deallocate our copy of environment
mov dx,OFFSET SignOnMsg
mov ah,9
int 21h ; Display message
mov dx,TSRParas ; Number of paragraphs to leave resident
mov ax,3100h
int 21h ; Go resident
Main2 ENDP
ComFile ENDS
END Main
-------------------------------- snip snip snip --------------------------------
This program demonstrates why interrupts must be locked out during manipulation
of hardware devices such as the RTC, because this TSR's int 8 handler explicitly
changes the address register in the RTC on each timer tick. If some foreground
code set the address register then made an access to the data register, without
disabling interrupts around the sequence, very occasionally an int 8 could be
signalled between the address register access and the data register access,
causing an incorrect value to be read or causing the time to change to a random
or meaningless value. This type of bug could be almost impossible to track
down. This is why it is important to follow these guidelines, though programs
that do not follow these guidelines often appear to work correctly.
This is also a good example of a TSR which lengthens processing of int 8 and
also lengthens this processing unevenly; most int 8 calls will be lengthened
only slightly, but every time the seconds change, the int 8 invocation will be
lengthened by a much greater amount. See section »» 6.16.1.
## 7.36 THE RTC INTERRUPT AND RELATED BIOS FUNCTIONS
The RTC interrupt is IRQ8, which maps to int 70 hex. It does not exist on the
PC and XT. This interrupt is invoked when any enabled interrupt source in the
RTC issues an interrupt, providing that IRQ8 is enabled in the secondary PIC's
Interrupt Mask Register (section »» 6.10) and IRQ2, the cascade interrupt, is
enabled in the primary PIC's IMR. See section »» 7.35.4 for info on how to
enable and disable the four interrupt sources in the RTC.
Usually, only the Alarm and the Periodic Interrupt triggers on the RTC are ever
used. The RTC interrupt is used in three ways:
■ The 24-hour Alarm signal from the RTC (see section »» 3.4),
■ The Event Wait and Delay functions of the BIOS (section »» 7.36.1),
■ User programs (e.g. slow-down programs or programs that measure
the execution time of other programs).
The Alarm signal uses the Alarm function of the RTC (obviously), and this
interrupt source is only enabled if the appropriate BIOS function has been
called to enable the alarm function.
The other two uses of int 70h involve the periodic interrupt from the RTC,
which is operated at 1024 interrupts per second (see section »» 7.35.3),
giving an interrupt every 976.5625 microseconds. This interrupt source on the
RTC is only enabled when the BIOS Event Wait or Delay functions are requested,
unless explicitly enabled by a foreground program or TSR directly accessing
the RTC registers.
IRQ8 (int 70h) must also be enabled in the PIC IMR. Often it will be left
enabled, and the various interrupt sources will be controlled at the RTC, but
some BIOSes may also disable the interrupt level when it is no longer required.
## 7.36.1 THE BIOS EVENT WAIT AND DELAY FUNCTIONS
On AT and later machines, the BIOS provides an 'event wait' function and a
delay function, that use the RTC interrupt for timing.
The 'event wait' and delay functions use nine bytes of RAM in the BIOS data
area, which are defined as follows:
Address Type Description
0040:0098 Far ptr Pointer to byte to be set to 80h when event
wait completes
0040:009C DWord Counter (down-counter, microseconds)
0040:00A0 Byte Status:
00h = Idle
01h = Event Wait or Delay in progress
80h = Delay time elapsed (transitional)
The functions are as follows.
Set Event Wait : int 15h
Call with: AX = 8300 hex
CX = Time to wait (microseconds) hiword
DX = Time to wait (microseconds) loword
ES:BX = Pointer to flag to be set when complete
Returns: CF = Error indication (see below)
This function sets up control information in the BIOS data area, and starts an
'event wait' timeout by enabling IRQ8 (int 70h) in the PIC and enabling the
periodic interrupt via the RTC. It returns to the caller while the event wait
is in progress. The event wait is counted down in the background, by the
BIOS's IRQ8 (int 70h) handler. When the specified time elapses, the interrupt
handler sets the byte that was pointed to by ES:BX when the function was
invoked, to 80h, and the wait is complete.
If this function is called when an event wait or delay function (described
later) is in progress, it will return with carry set and ignore the request.
If the function is not supported by the BIOS, it will return with carry set
and AH set to 80h or 86h.
Cancel Event Wait : int 15h
Call with: AX = 8301 hex
Returns: CF = Error indication
This function cancels the event wait currently in progress. It disables the
periodic interrupt in the RTC and resets the event wait status byte at
0040:00A0 to zero.
Delay : int 15h
Call with: AH = 86 hex
CX = Time to delay (microseconds) hiword
DX = Time to delay (microseconds) loword
Returns: CF = Error indication
This function delays for the number of microseconds specified in CX and DX, and
returns with carry clear when the delay is complete. It returns with carry set
and AH set to 80h or 86h if the function is unsupported. It returns with carry
set if the delay function was called while an Event Wait or another Delay was
in progress.
This function uses the same data structure as the Event Wait function, but sets
the pointer that determines the byte to be set to 80h when the wait completes,
to point to the status byte.
The time for these functions is specified in microseconds, but the resolution
is only 977 us, since timing is done using the RTC interrupt. The periodic
interrupt in the RTC is not resynchronised by the function, so there is also
a 977 us uncertainty at the start of the time period, which limits accuracy of
short delays. Also, IRQ8 (int 70h) occurs every 976.5625 us but the handler
subtracts 977 from the count each time. This is an error of 0.0448% (448 ppm,
38.71 seconds per day). This error is cumulative and could become significant
on long delays (it will make the delay shorter than expected).
If any software locks out interrupts for more than 977 us at a time, interrupts
will be missed and the time period will be extended. The BIOS joystick reading
function (section »» 10.4.2) and the joystick position reading function given
in section »» 10.4.4 may cause this problem.
I have heard that the Event Wait function is used by the hard disk and floppy
disk BIOS code, but I don't know the details. Info is welcomed. (*)
## 7.36.2 THE BIOS RTC INTERRUPT HANDLER
The BIOS has its own IRQ8 (int 70h) handler, which counts down the Event Wait
or Delay time value and sets the flag byte to 80h when the time expires (see
section »» 7.36.1 for the gory details). The handler makes use of the three
variables in the BIOS data area which are described in section »» 7.36.1.
The exact behaviour may vary from one BIOS to another but is something like:
First, check whether an alarm interrupt occurred (using Register C of the RTC,
see section »» 7.35.5). If so, invoke int 4Ah (see section »» 3.4). Then
check whether a periodic interrupt occurred. If so, subtract 977 from the
unsigned long microsecond counter (see section »» 7.36.1). If this resulted in
the long variable borrowing (i.e. wrapping around from a small positive number
to a negative number), zero the status flag (see section »» 7.36.1), disable the
regular interrupt source in the RTC, then set to 80 hex the byte pointed to by
the far pointer in the BIOS data area (see section »» 7.36.1 again).
The RTC interrupt handler in some BIOSes may unconditionally turn off the
periodic interrupt enable in the RTC if the status flag (see section »» 7.36.1
again) is zero, to avoid unnecessary processor overhead (1024 interrupts per
second can be significant).
## 7.36.3 USING THE RTC INTERRUPT
The RTC interrupt is int 70h (IRQ8). When a program uses the RTC interrupt,
it should chain to the original handler, because the BIOS may be in the middle
of a Delay or Event Wait. The BIOS's int 70h handler may interfere with your
program, by turning off the periodic interrupt enable in the RTC, so your int
70h handler must re-enable it after calling the BIOS's handler.
The BIOS's handler will also count down the microseconds counter (see sections
»» 7.36.1 and »» 7.36.2) and when it borrows, will set a memory variable at the
address pointed to by the pointer in the BIOS data area, to 80 hex. This may
not be desirable, as this pointer may be uninitialised, or may point to a
variable in a program that is no longer running, etc. Therefore your program
should be careful to prevent the BIOS's handler from doing this. This is
demonstrated in the sample program in section »» 7.36.4.
## 7.36.4 SAMPLE PROGRAM: USING THE RTC INTERRUPT
-------------------------------- snip snip snip --------------------------------
/*
Sample program #11
Demonstrates using the RTC periodic interrupt
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save and assemble the critical error module CRIT_ERR
Save this sample code to SAMPLE11.C
Compile this module with:
bcc -c -I<inc_path> -ms sample11.c
Link the modules with:
tlink /c /x <c0_path>\c0s.obj sample11.obj crit_err.obj,
sample11, nul, <lib_path>\cs
Where inc_path is the path to your C header files, c0_path is the path to your
startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, and cli */
#include <bios.h> /* Needed for bioskey() */
#include <dos.h> /* Needed for inportb(), outportb(), etc */
#include <io.h> /* Needed for _write() */
#include <stdio.h> /* Needed for printf() */
#include <stdlib.h> /* Needed for exit() */
#define FALSE 0
#define TRUE 1
#define STDERR 2 /* DOS handle for standard error */
static unsigned long rtcticks; /* Counter for RTC interrupts */
void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */
unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */
typedef void interrupt (far *intfuncp)(); /* Pointer to an int handler */
intfuncp old_int70 = (intfuncp)0xFFFFFFFFL;
unsigned char read_rtc_register(unsigned char reg_num) {
unsigned char rv;
asm pushf;
asm cli;
outportb(0x70, reg_num);
asm jmp SHORT $+2
asm jmp SHORT $+2
asm jmp SHORT $+2
rv = inportb(0x71);
asm popf;
return rv;
}
void write_rtc_register(unsigned char reg_num, unsigned char value) {
asm pushf;
asm cli;
outportb(0x70, reg_num);
asm jmp SHORT $+2
asm jmp SHORT $+2
asm jmp SHORT $+2
outportb(0x71, value);
asm popf;
return;
}
void enable_rtc_int(void) {
asm pushf;
asm cli;
write_rtc_register(0x0B, read_rtc_register(0x0B) | 0x40);
outportb(0xA1, inportb(0xA1) & 0xFE);
outportb(0x21, inportb(0x21) & 0xFB);
asm popf;
return;
}
void interrupt int70_handler(void) {
if (read_rtc_register(0x0C) & 0x40)
++rtcticks; /* Increment RTC tick counter */
(old_int70)(); /* Chain to BIOS int 70h handler */
enable_rtc_int(); /* Make sure RTC int is still enabled */
if (* ((unsigned int far *)MK_FP(0x40, 0x9E)) > 0xFFFD)
* (unsigned int far *)MK_FP(0x40, 0x9E) = 0xFFFF;
return; /* From interrupt */
}
void abort_cleanup(int dos_is_safe) {
if (dos_is_safe) {
if (old_int70 != (intfuncp)0xFFFFFFFFL) {
setvect(0x70, old_int70);
old_int70 = (void far *)0xFFFFFFFFL;
}
}
else {
if (old_int70 != (intfuncp)0xFFFFFFFFL) {
*((intfuncp far *)MK_FP(0, 0x70 << 2)) = old_int70;
old_int70 = (void far *)0xFFFFFFFFL;
}
}
return;
}
void interrupt ctrl_c_handler(void) {
static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n";
if (is_at_crit_prompt())
abort_cleanup(FALSE);
else {
abort_cleanup(TRUE);
_write(STDERR, &message, sizeof(message));
}
exit(255);
}
void main(void) {
unsigned long msecs, secs;
unsigned int partial;
printf("Sample program #11 - Demonstrates using the RTC interrupt\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
printf("Press <Esc> to exit\n\n");
crit_err_intercept(); /* Trap critical errors */
setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */
* (unsigned int far *)MK_FP(0x40, 0x9E) = 0xFFFF;
old_int70 = getvect(0x70);
setvect(0x70, int70_handler);
asm cli; /* 1024 interrupts per second */
write_rtc_register(0x0A, (read_rtc_register(0x0A) & 0xF0) | 0x06);
asm sti;
enable_rtc_int();
while (1) {
asm cli;
msecs = rtcticks;
asm sti;
msecs *= 125; /* Calculate * 125 / 128 */
msecs >>= 6;
if (msecs & 1)
++msecs; /* Round up */
msecs >>= 1;
secs = msecs / 1000;
partial = msecs % 1000;
printf("%ld.%03d seconds\r", secs, partial);
if (bioskey(1))
if ((bioskey(0) & 0xFF) == 27)
break;
}
setvect(0x70, old_int70);
old_int70 = (void far *)0xFFFFFFFFL;
exit(0);
}
-------------------------------- snip snip snip --------------------------------
The int70_handler() function first checks that this int 70h is caused by the
periodic interrupt, and if so, it increments its counter. It then calls the
BIOS int 70h handler unconditionally. The BIOS handler will send the EOI to
both PICs and will probably turn off the periodic interrupt enable flag in the
RTC, so this handler turns the periodic interrupt back on. It also tries to
ensure that no problems will occur with the timeout detection of the BIOS's
handler. When the long microseconds counter at 0040:009C is decremented below
zero by the BIOS handler, the BIOS handler writes to a memory variable pointed
to by the pointer at 0040:0098, and this pointer may not have been initialised.
By keeping the microsecond count at 0xFFFFxxxx, this routine prevents this
problem. The mainline also sets the microsecond counter to 0xFFFFxxxx. This
should allow the Delay and Event Wait functions to be used without interference
from this program.
## 7.37 USING CTC CHANNEL ONE AND REFRESH DETECT
My thanks to William Luitje (luitje@m-net.arbornet.org) for introducing me to
this technique. William reports that it is used by the AMI BIOS during floppy
disk operations.
As shown in section »» 7.5, bit 4 of Port B at I/O address 61 hex on an AT and
later machine is a read-only bit carrying a signal called Refresh Detect.
This signal comes from a 'T' (toggle) flip-flop which is clocked by the refresh
trigger signal, which comes from CTC channel one. I assume it is used by the
BIOS POST (Power-on self-test) to check that CTC channel one is functioning
correctly (IBM's paranoid self-test code has to test every single logic gate
on the entire motherboard - this from the people who created the error message
"Keyboard error or no keyboard present - press F1 to continue" :-)
Assuming that the RAM refresh rate has not been changed (see section »» 7.4.3),
this bit will toggle (change from 0 to 1 or from 1 to 0) once every 15.0857
microseconds (the exact value is 216/14.31818), and Port B can be polled in a
loop to implement a delay of any length. For short delays, with interrupts
locked out, this gives an accurate and very convenient relative delay mechanism.
However, for long delays, it would be naughty to leave interrupts locked out for
the entire delay period, and interrupts will cause gaps in the polling process,
slightly lengthening the delay (it will wait longer than expected).
There are several caveats. This method will not work on PCs and XTs. Also it
will not work in an environment where Port B is emulated (for example, under
OS/2 and probably any other virtual DOS machine). Finally, if the DRAM refresh
period has been changed, the timing will be changed proportionately.
## 7.37.1 SAMPLE PROGRAM: TIMING THE REFRESH DETECT SIGNAL
This program uses CTC channel 2 to measure a sampling period of half a second
with interrupts locked out, and counts the number of transitions on the Refresh
Detect signal during this period. It displays the value after each half-second
sample, and repeats the sample continuously until Ctrl-C is pressed.
Warnings about using this program: It will not work on an emulated machine,
i.e. under OS/2 or any other multi-tasking operating system that gives it a
virtual DOS machine. The program will not run on an old PC or XT; an AT or
later machine is required. The program will disrupt the DOS time of day, so
the machine should be rebooted after running this program if that is a problem.
Also, it does not check the absolute accuracy of the Refresh Detect signal;
the signal being measured and the sample timer are both derived from the same
clock source.
The joystick reading sample program in section »» 10.4.4 also demonstrates
the Refresh Detect signal used as a timing reference.
-------------------------------- snip snip snip --------------------------------
NAME SAMPLE12
; Sample program #12
; Demonstrates timing the Refresh Detect signal
; Part of the PC Timing FAQ / Application notes
; By K. Heidenstrom (kheidens@actrix.gen.nz)
;
; This program assembles into SAMPLE12.COM, a small program which measures the
; number of DRAM refreshes in a half-second interval. It uses CTC channel 2
; in mode 3 to measure half-second sample periods, and counts the number of
; transitions on the Refresh Detect signal on Port B bit 4. After each
; half-second sample, the value is displayed, and the program repeats itself.
; To terminate the program, press any key and wait.
;
; Save this file to SAMPLE12.ASM and assemble with:
; masm sample12;
; link sample12;
; exe2bin sample12.exe sample12.com
; or
; tasm sample12;
; tlink /t sample12;
;
CTC2Divisor = 47727 ; 40ms
CTC2Toggles = 25 ; Number of CTC channel 2 toggles
; expected in half a second
ComFile SEGMENT
ASSUME cs:ComFile,ds:ComFile,es:nothing,ss:nothing
ORG 100h ; Com-type file
Main PROC near
jmp Main2 ; Skip
Main ENDP
InitialMsg DB "Sample program #12 - Demonstrates timing the Refresh Detect signal",13,10
DB "Part of the PC Timing FAQ / Application notes",13,10
DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10,13,10,"$"
NotATMsg DB "Refresh Detect is supported on ATs and later machines; this machine appears",13,10
DB "to be a PC or XT. The PC and XT do not support Refresh Detect.",13,10,"$"
ExplanationMsg DB "The numbers displayed are the counts of DRAM refreshes in a 1/2-second sample",13,10
DB "period. For the standard DRAM refresh rate of 15.0857us, this number should",13,10
DB "be about 33144. If you have run a program to slow down the DRAM refresh, the",13,10
DB "numbers will be lower.",13,10,13,10
DB "To terminate this program, press any key and wait",13,10
NewlineMsg DB 13,10,"$"
Main2 PROC near
mov dx,OFFSET InitialMsg ; Opening message
mov ah,9
int 21h ; Display it
; Determine machine type (code from section »» 7.5)
pushf ; Keep interrupt flag
mov cx,400h ; Six attempts (top bits of CH)
cli ; Lock out interrupts during this stuff
in al,61h ; Get Port B contents
jmp SHORT $+2 ; Short delay
mov ah,al ; Original value to AH
Flip61Loop: xor ah,10000000b ; Flip top bit
mov al,ah ; Get value to AL
out 61h,al ; Write value to port
jmp SHORT $+2 ; Short delay
jmp SHORT $+2 ; Short delay
in al,61h ; Read it back
xor al,ah ; Set bit 7 if value didn't stay
shl al,1 ; Shift bit into carry
rcl cx,1 ; Shift bit into bottom of CX
jnc Flip61Loop ; Loop if more flips (six in total).
popf ; Restore interrupt flag
test cl,cl ; Was port read/write? Zero if so.
jnz MachineAT ; If it's an AT, continue
mov dx,OFFSET NotATMsg
mov ah,9
int 21h
mov al,1 ; Errorlevel
jmp SHORT Terminate
MachineAT: mov dx,OFFSET ExplanationMsg
mov ah,9
int 21h
in al,61h ; Get Port B
and al,11111101b ; Turn off speaker enable
or al,00000001b ; Turn on Timer 2 Gate
out 61h,al
mov al,0B6h ; Set CTC channel 2 for mode 3,
out 43h,al ; divisor of 47727, giving 20ms
jmp short $+2 ; high, 20ms low
mov al,LOW CTC2Divisor
out 42h,al
jmp short $+2
mov al,HIGH CTC2Divisor
out 42h,al
jmp short $+2
MainLoop: mov cl,3 ; Set up shift count for later
mov bx,CTC2Toggles ; Number of channel 2 transitions
xor dx,dx ; Counter for refreshes
cli ; Lock out interrupts during sample
Loop1: in al,61h ; Read Port B
test al,00100000b ; Test CTC channel 2 readback
jz Loop1 ; Wait until high
Loop2: in al,61h ; Read Port B
test al,00100000b ; Test CTC channel 2 readback
jnz Loop2 ; Wait until low
mov ah,al ; Keep old value in AH
Loop3: in al,61h ; Read Port B
xor al,ah ; Find different bits
test al,00110000b ; Either bit changed?
jz Loop3 ; If not, loop
xor ah,al ; Keep new value
shl al,cl ; Bit 5 into carry
sbb bx,0 ; Decrement BX if T2 output changed
jz Done ; If waited the full sample time
shl al,1 ; Bit 4 into carry
adc dx,0 ; Increment DX if refresh occurred
jmp SHORT Loop3 ; Loop
Done: sti ; Interrupts back on
mov ax,dx ; Get refresh counter
call Mach16_DecASC ; Convert to decimal and display
mov dx,OFFSET NewlineMsg ; CR/LF message
mov ah,9
int 21h ; Display it
mov ah,1 ; Test for keypress pending
int 16h
jz MainLoop ; If no key pending
xor ah,ah ; Zero
int 16h ; Clear out the key
xor al,al ; Errorlevel 0
Terminate: mov ah,4Ch ; Terminate with errorlevel
int 21h ; Call DOS
int 20h ; In case DOS-1 (!)
Main2 ENDP
Mach16_DecASC PROC near
; Func: Convert machine 16-bit unsigned value
; to ASCII decimal representation and
; output via DOS function 2
; In: AX = Value to output
; Out: None
; Lost: AX BX CX DX
xor cx,cx ; Zero digit counter
Mach16_DecASC1: xor dx,dx ; Clear high word of value in DX|AX
mov bx,10 ; Base
div bx ; Divide by 10
add dl,"0" ; DL is remainder, convert to ASCII
push dx ; Store on stack
inc cx ; Increment char counter
test ax,ax ; Any more digits left?
jnz Mach16_DecASC1 ; If so, loop
Mach16_DecASC2: pop dx ; Get char back
mov ah,2 ; Print char
int 21h ; Call DOS
loop Mach16_DecASC2 ; Loop for all chars
ret ; Done
Mach16_DecASC ENDP
ComFile ENDS
END Main
-------------------------------- snip snip snip --------------------------------
## 7.37.2 SAMPLE CODE: DELAY(MILLISECONDS) FUNCTION USING REFRESH DETECT
This function uses the Refresh Detect signal to provide a delay(milliseconds)
function. This function does not check that the refresh channel is operating
with the correct divisor. It also does not check that it is running on an AT
or later machine with the required Port B hardware. If required, these checks
should be done at the start of the program that will use this function.
-------------------------------- snip snip snip --------------------------------
Params = 4 ; USE 6 FOR FAR CODE MODELS!
_delay PROC near
push bp ; Preserve BP
mov bp,sp ; Address stacked parameters
mov cx,[bp+Params] ; Get loword of number of milliseconds
mov dx,[bp+Params+2] ; Get hiword
mov bx,61714 ; Initialise negative count register
in al,61h ; Read Port B initially
mov ah,al ; To AH
jmp SHORT DelayDecr ; Decrement count and loop if nonzero
DelayLoop: in al,61h ; Read Port B
xor al,ah ; Get different bits
test al,00100000b ; Did Refresh Detect toggle?
jz DelayLoop ; If not, keep waiting
xor ah,00100000b ; Toggle last known state flag
sub bx,931 ; Approximating the number of Refresh
jnb DelayLoop ; of Refresh Detect toggles per
add bx,61714 ; millisecond as 61714 / 931
DelayDecr: sub cx,1 ; One millisecond has elapsed
sbb dx,0 ; Borrow into hiword
jnb DelayLoop ; If more milliseconds remaining
pop bp ; Restore BP from caller
ret
_delay ENDP
-------------------------------- snip snip snip --------------------------------
The declaration for the above function is:
void delay(unsigned long milliseconds)
The actual number of Refresh Detect toggles per millisecond is 14318.18/216, or
about 66.3. The above function approximates this ratio to be 61714/931, which
contributes an error of 0.085767 ppm, less than 1/100th typical crystal error.
The longest delay that can be generated (milliseconds = 0xFFFFFFFF) is 49 days,
17 hours, 2 minutes, and 47.295 seconds. For this duration, the error
contributed by the approximation is about 0.368 seconds.
The delay(milliseconds) function may be called with interrupts enabled or with
interrupts disabled. It does not modify the state of the interrupt flag during
its execution.
If it runs with interrupts enabled, the actual length of the delay will normally
be longer than specified, due to gaps in processing caused by the timer tick
interrupt and any other active interrupts (keyboard interrupt, serial port
interrupt, network card interrupt, etc).
If it runs with interrupts locked out, it will give an accurate delay, but it
may disrupt the normal operation of the machine by preventing interrupts from
being processed for an excessive length of time - see sections »» 6.15 to
»» 6.19 for more information on this problem.
The uncertainty is one refresh period, or about 15.0857 microseconds. The
overhead is a few microseconds on a fast machine, longer on a slow machine.
## 8 SPEEDING UP THE TIMER TICK
Note: This section makes many references to earlier sections. I recommend that
if you are not familiar with the normal operation of the CTC, the timer tick
interrupt, general interrupt considerations, and interrupt chaining, you should
first read the related sections and any other sections that seem relevant.
Increasing the timer tick rate involves the following steps:
■ Intercept int 8, redirecting it to your new int 8 handler
■ Intercept and handle the Ctrl-C and Critical Error interrupts so that
the int 8 vector can be restored if the program is terminated due to
a Ctrl-C or a critical error
■ Reprogram the CTC channel zero divisor for the new interrupt rate
■ Maintain a counter within your int 8 handler to schedule chaining to
the original int 8 handler
■ Restore int 8 and restore the normal divisor upon termination
See section »» 6.3 for details of how to intercept an interrupt. See section
»» 6.31 for information on chaining to the old interrupt handler, and section
»» 5 and subsections for information about the Ctrl-C and Critical Error
interrupts and how to handle them. See section »» 7.10 for how to program the
divisor. See section »» 6.31 for details on how to chain to the original int
8 handler, and sections »» 6.28 and »» 6.28.1 for information on ending
interrupt routines when they are not chained. The comments in section »» 6.15
and section »» 6.16 and subsections regarding interrupt jitter also apply when
the timer tick is operated at a faster rate, because the maximum period for
which interrupts can be locked out without loss of a timer interrupt becomes
shorter as the timer interrupt rate is increased. Changing the divisor and/or
operating mode of CTC channel 0 may also break the BIOS's joystick reading
functions (see section »» 10.4.2). Also see section »» 8.4.
The technique of speeding up the timer tick should not be used in TSRs because
foreground programs are at liberty to use and reprogram the CTC chip for their
own purposes.
## 8.1 THE FAST TICK INT 8 HANDLER
Having reprogrammed the timer tick interrupt to operate at a higher speed, you
must ensure that other software that uses int 8 (see section »» 6.1) is called
at the correct rate, i.e. 18.2065 times per second. This is achieved with a
counter variable, which duplicates the behaviour of CTC channel zero when it is
operating with the normal divisor of 65536 (see section »» 7.4 and subsections).
This operates by maintaining a 16-bit variable which accumulates CTC clock
periods and will overflow after 65536 CTC clocks, indicating that another
54.9254 ms have elapsed. This variable is maintained by the new int 8
handler. Every time int 8 is signalled, the new channel zero divisor value
(which represents the number of CTC clocks since the last int 8) is added
into this variable, and if the variable carries (i.e. exceeds 65535 and wraps
around), the old int 8 handler is called (i.e. is scheduled). If the variable
does not wrap around, then the old int 8 handler is not called.
For example, assume CTC channel zero is operating with a divisor of 1234
(decimal). This will give a fast tick rate of 1193181.66666... / 1234, or
about 967 ticks per second. Each time the new int 8 handler is triggered,
1234 CTC clocks have elapsed (since the last time it was triggered), so we
add 1234 into the scheduler variable, representing the number of CTC clocks
that have just elapsed.
When the variable wraps around (i.e. the processor's carry flag is set after
the addition), another 65536 CTC clocks have elapsed, so it is time to chain
to the original int 8 handler, which expects to be called every 65536 CTC
clocks (the "slow tick" rate, if you like). Thus the variable mimics the CTC
channel zero counting register when programmed for a divisor of 65536.
Of course the slow ticks (calls to the old int 8 handler) will not be perfectly
evenly spaced, but in almost all applications, variations are acceptable as
long as they are not cumulative, and they will not be cumulative (unless fast
ticks are missed, see section »» 6.16 and subsections), and if the new tick
rate is high (like the example above) they will be fairly even. The worst slow
tick jitter will occur with divisors near to 32768, where calls to the slow
tick handler could be up to almost 32768 CTC clocks early or late. Even this
will not be a problem under DOS, in most circumstances.
## 8.2 THE INTERFACE WITH THE MAINLINE
Generally the fast tick handler will have some sort of interface with the
mainline (the foreground process) of your program. Typically this will be
implemented via shared variables. These variables transfer control information
from the mainline to the interrupt routine (as in the Morse code player example
program described later), or may transfer status or time information from the
interrupt routine to the mainline (as in the one millisecond timer program also
described later), or a combination of both.
The shared variables can be put in either the code segment or the data segment.
For a COM file (tiny model) the segments are the same, and this makes things
quite convenient. See section »» 6.32.1 for more information.
## 8.3 WRITING A FAST TICK HANDLER
Fast tick handlers are often written in assembly language as it is more
convenient and more efficient, though the latter advantage is largely mooted
by the speed of modern processors.
Refer to section »» 6.32 and subsections for a discussion of guidelines that
must be applied when writing an interrupt handler in assembly language. The
fast tick handler must follow these guidelines.
After the housekeeping instructions, the fast tick interrupt handler should
perform the function that it is required for, then before it exits, it should
handle chaining to the slow tick interrupt handler. This involves adding the
divisor value into the scheduler variable and deciding whether to chain to the
slow tick handler or not.
If it decides to chain, it can use the JMP chaining method (see section »» 6.31
for details). If it does not chain, it must send an EOI signal to the PIC (see
section »» 6.28) and return with an IRET. To support Microchannel machines, it
may be necessary to acknowledge the int 8 - see section »» 6.28.1.
Here is an example fast tick interrupt handler written in assembler for tiny
model (i.e. a COM file). It increments the FastTickCount variable on each
fast tick. This variable is for use by the mainline.
-------------------------------- snip snip snip --------------------------------
FastTickRate EQU 1234 ; This is the new fast tick rate
FastTickCount DW 0 ; Counter variable for use by mainline
SlowTickSched DW 0 ; Schedule control var for slow tick
ASSUME cs:_TEXT,ds:nothing,es:nothing,ss:nothing
NewInt8Handler PROC far
pushf ; Keep flags
push ax ; Keep AX
; Push any other registers you will modify
inc FastTickCount ; This is the 'action' in this example
; Pop any other registers you pushed, in
; reverse order, but do not pop AX or Flags
add SlowTickSched,FastTickRate ; Add ticks into variable
jnc NoSchedule ; If it didn't carry
; Another 54.9254 ms has elapsed - chain to the slow tick handler
pop ax ; Restore AX
popf ; Restore flags
DB 0EAh ; JMP xxxx:yyyy
Old08Ofs DW 0 ; Vector to original handler - Offset
Old08Seg DW 0 ; Segment
; Not time to chain yet - send EOI and return. Note - may not support
; Microchannel machines which may require a hardware int 8 acknowledge
; signal to be issued.
NoSchedule: mov al,20h ; EOI code for PIC
out 20h,al ; Send
pop ax ; Restore AX
popf ; Restore flags
iret
NewInt8Handler ENDP
-------------------------------- snip snip snip --------------------------------
Of course to use this interrupt handler, you must have first intercepted int 8
and reprogrammed CTC channel zero with a divisor of 1234. Also for safety you
must have intercepted the DOS Ctrl-C and Critical Error interrupts so that the
original channel zero divisor, and original int 8 handler address, can be
restored if the program terminates for either of these reasons.
## 8.4 COMMENTS ON FAST TIMER TICK INTERRUPTS
{JAM} makes some good comments about this (slightly paraphrased):
"Speeding up the timer tick interrupt presents two problems. The first is the
increased load on the CPU, and the second is that any routine that disables
interrupts for over twice the fast tick interrupt period will cause a missed
interrupt. Masking for less then the interrupt period will cause interrupt
delivery jitter and maybe the loss of a fast tick interrupt.
"In the days of 8 MHz ATs, the former problem was the dominant one; now with
faster computers and more complicated operating systems and TSR programs, the
more subtle second problem dominates."
Klaus Hartnegg (klaus@mailserv.brain.uni-freiburg.de) tried using a fast timer
interrupt at 4, 6, and 8 kHz, and reports "serious problems with interrupts
generated by network and keyboard (especially bad with DOS's KEYB.COM driver,
a lot better with a freeware replacement). I have come to the conclusion that
it's probably not possible to rely on such a high frequency timer interrupt.
There are too many periods of time with disabled interrupts that cause lost
interrupts."
## 8.5 SAMPLE PROGRAM: MORSE PLAYER USING FAST TIMER TICK
The following program demonstrates the techniques involved in operating the
timer tick at a higher speed. The tickdiv variable contains the new divisor
that is programmed into CTC channel zero. The int8sched variable controls
chaining to the original handler. It duplicates the normal action of CTC
channel zero when programmed with a divisor of 65536, as described in section
»» 8.3.
A single queue interface is used to transfer control information from the
mainline to the timer tick handler. The int 8 routine has full control of the
speaker hardware, and generates beeps using CTC channel two in response to
control words sent from the mainline via the queue.
Most of the code is fairly self-explanatory. The fast tick interrupt is
chosen according to the speed selected via the command line parameter, which
may be any number from 1 to 99. The speed doubles for each decade. There are
ten divisors, contained in the tickdivs[] array. Within each decade of speed
numbers, the tickdivs[] values give a smoothly increasing speed, then from one
decade to the next, the number of interrupts per 'dit' or 'dah' goes up in
powers of two, resulting in a smooth speed scale, with each decade giving a
2:1 increase in playback speed. For testing, a speed of 50 is reasonable.
By the way, when speaking Morse code, speak a '.' as 'dit' and '-' as 'dah',
and join a dit to any following dit or dah in the same letter code. So, for
example, the Morse code for the letter C ("-.-.") is spoken "dah-di-dah-dit".
A proper Morse code player would be much more powerful, but this program is
coming dangerously close to being useful. I will be more careful in future :-)
See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-------------------------------- snip snip snip --------------------------------
/*
Sample program #13
Demonstrates fast timer tick
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save and assemble the critical error module CRIT_ERR
Save this sample code to SAMPLE13.C
Compile this module with:
bcc -c -I<inc_path> -ms sample13.c
Link the modules with:
tlink /c /x <c0_path>\c0s.obj sample13.obj crit_err.obj,
sample13, nul, <lib_path>\cs
Where inc_path is the path to your C header files, c0_path is the path to your
startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, and cli */
#include <bios.h> /* Needed for bioskey() */
#include <dos.h> /* Needed for MK_FP() */
#include <io.h> /* Needed for _write() */
#include <stdio.h> /* Needed for printf() */
#include <stdlib.h> /* Needed for exit() */
#define FALSE 0
#define TRUE 1
#define STDERR 2 /* DOS handle for standard error */
#define MORSEBUFSIZE 128 /* Number of entries in morse data buffer */
#define BEEP_DIVISOR 2000 /* Freq = 1193182 / BEEP_DIVISOR */
#define DIT_LENGTH 1 /* Length of a dit */
#define DAH_LENGTH 3 /* Length of a dah */
#define DIT_SPACING 1 /* Spacing between dits/dahs within a letter */
#define LETTER_SPACING 3 /* Spacing between letter codes */
#define WORD_SPACING 6 /* Spacing between words */
#define STOP_SPACING 10 /* Spacing after a full stop (period) '.' */
#define ONOFF 0x8000 /* Top bit controls tone on or off */
static unsigned int tickdivs[10] = {
16384, 15287, 14263, 13308, 12417,
11585, 10809, 10086, 9410, 8780
};
static unsigned int morsebuf[MORSEBUFSIZE]; /* Communication between
mainline and int 8 stuff */
/* Characters for morsecode array:
"" Ignore this code completely
"w" Word space - enforce a word spacing at this point
"s" Stop space - enforce a full stop spacing at this point
".--." Actual code to send, using letter spacing at end */
static unsigned char morsecode[128][7] = {
"", "", "", "", "", "", "", "w", /* 0 to 7 */
"", "w", "w", "", "w", "w", "", "", /* 8 to 15 */
"", "", "", "", "", "", "", "", /* 16 to 23 */
"", "", "", "", "", "", "", "", /* 24 to 31 */
"w", "", "", "", "", "", "", "", /* ' ' to ''' */
"", "", "", "", "", "", "s", "", /* '(' to '/' */
"-----", ".----", "..---", "...--", "....-", /* 0 to 4 */
".....", "-....", "--...", "---..", "----.", /* 5 to 9 */
"w", "", "", "", "", "", "", /* ':' to '@' */
".-", "-...", "-.-.", "-..", ".", /* 'A' to 'E' */
"..-.", "--.", "....", "..", ".---", /* 'F' to 'J' */
"-.-", ".-..", "--", "-.", "---", /* 'K' to 'O' */
".--.", "--.-", ".-.", "...", "-", /* 'P' to 'T' */
"..-", "...-", ".--", "-..-", "-.--", "--..", /* 'U' to 'Z' */
"", "", "", "", "", "", /* '[' to '`' */
".-", "-...", "-.-.", "-..", ".", /* 'a' to 'e' */
"..-.", "--.", "....", "..", ".---", /* 'f' to 'j' */
"-.-", ".-..", "--", "-.", "---", /* 'k' to 'o' */
".--.", "--.-", ".-.", "...", "-", /* 'p' to 't' */
"..-", "...-", ".--", "-..-", "-.--", "--..", /* 'u' to 'z' */
"", "", "", "", "" /* '{' to Del */
};
static unsigned int timescaler; /* Time range scaler */
static unsigned int inptr;
static volatile unsigned int outptr; /* Offsets into morsebuf */
static unsigned int tickdiv; /* Actual chosen tick divisor */
void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */
unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */
typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */
intfuncp old_int8 = (intfuncp)0xFFFFFFFFL;
/* Communication between the mainline and the int 8 handler is via the morsebuf
array, which is used as a queue. Each entry in morsebuf is a 16-bit unsigned
int. The top bit determines whether the beeping sound should be turned on
(if set) or off (if clear), and the remaining bits determine how many fast
ticks the int 8 routine will wait after actioning the top bit, before it
fetches the next word from morsebuf. Access to morsebuf is controlled by
the in and out pointers, which are actually offsets, not pointers. When
these are equal, the queue is empty. */
void interrupt int8_handler(void) {
static unsigned int int8counter = 0;
static unsigned int int8sched = 0;
if (int8counter)
--int8counter;
if ((int8counter == 0) && (outptr != inptr)) { /* Data there */
int8counter = morsebuf[outptr];
++outptr;
if (outptr >= MORSEBUFSIZE) /* Bump out ptr */
outptr = 0;
if (int8counter & ONOFF) { /* Turn sound on */
outportb(0x43, 0xB6); /* Ch 2, mode 3 */
outportb(0x42, (BEEP_DIVISOR & 0xFF)); /* Lobyte */
outportb(0x42, (BEEP_DIVISOR >> 8)); /* Hibyte */
outportb(0x61, inportb(0x61) | 0x03); /* Speaker on */
}
else { /* Turn sound off */
outportb(0x61, inportb(0x61) & 0xFC); /* Speaker off */
}
int8counter &= (~ ONOFF); /* Remove on/off bit */
}
int8sched += tickdiv;
if (int8sched < tickdiv) { /* If carried */
(old_int8)(); /* Chain to BIOS */
}
else
/** note - may not support Microchannel machines */
outportb(0x20, 0x20); /* Send EOI if not chaining */
return; /* From interrupt */
}
void restore_normal(void) {
asm pushf;
asm cli;
outportb(0x43, 0x36);
outportb(0x40, 0);
outportb(0x40, 0); /* Restore normal divisor */
outportb(0x61, inportb(0x61) & 0xFC); /* Speaker off */
asm popf;
return;
}
void abort_cleanup(int dos_is_safe) {
if (dos_is_safe) {
if (old_int8 != (intfuncp)0xFFFFFFFFL) {
setvect(0x08, old_int8);
old_int8 = (intfuncp)0xFFFFFFFFL;
}
}
else {
if (old_int8 != (intfuncp)0xFFFFFFFFL) {
*((intfuncp far *)MK_FP(0, 0x08 << 2)) = old_int8;
old_int8 = (intfuncp)0xFFFFFFFFL;
}
}
restore_normal();
return;
}
void interrupt ctrl_c_handler(void) {
static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n";
if (is_at_crit_prompt())
abort_cleanup(FALSE);
else {
abort_cleanup(TRUE);
_write(STDERR, &message, sizeof(message));
}
exit(255);
}
void poll_exit(void) {
if (bioskey(1)) {
if ((bioskey(0) & 0xFF) == 27) {
setvect(0x08, old_int8);
old_int8 = (intfuncp)0xFFFFFFFFL;
restore_normal();
exit(0);
}
}
return;
}
void putmorse(unsigned int codeval) {
unsigned int tempptr;
poll_exit();
tempptr = inptr + 1;
if (tempptr >= MORSEBUFSIZE)
tempptr = 0;
while (outptr == tempptr)
poll_exit(); /* Wait for space in the queue */
codeval = (((codeval & (~ONOFF)) << timescaler) | (codeval & ONOFF));
morsebuf[inptr] = codeval;
inptr = tempptr;
return;
}
void playmorse(char * str) {
char ch;
char * cp;
unsigned int was_word;
was_word = FALSE;
cp = str;
while ((ch = *cp++) != '\0') {
switch (ch) {
case 'w' :
putmorse(WORD_SPACING);
break;
case 's' :
putmorse(STOP_SPACING);
break;
case '.' :
putmorse(DIT_LENGTH | ONOFF);
putmorse(DIT_SPACING);
was_word = TRUE;
break;
case '-' :
case '_' :
putmorse(DAH_LENGTH | ONOFF);
putmorse(DIT_SPACING);
was_word = TRUE;
break;
}
}
if (was_word)
putmorse(LETTER_SPACING);
return;
}
void main(int argc, char * argv[]) {
unsigned int speedrange;
int ch;
FILE * infile;
printf("Sample program #13 - Morse code player demonstrating fast timer tick\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
if ((argc < 3) || (strlen(argv[1]) != 2)) {
printf("Usage: SAMPLE13 speed filename\n\n");
printf("\tspeed = 10 to 99, speed doubles each decade\n");
printf("\tfilename = name of file to be played\n");
exit(1);
}
timescaler = 8 - (argv[1][0] - '1'); /* Shift count for timings */
speedrange = argv[1][1] - '0'; /* Fine speed, 0-9 */
if ((timescaler > 8) || (speedrange > 9)) {
printf("Speed out of range\n");
exit(2);
}
tickdiv = tickdivs[9 - speedrange];
infile = fopen(argv[2], "r");
if (infile == NULL) {
printf("Could not open input file '%s'\n", argv[2]);
exit(4);
}
printf("Press <Esc> to exit\n");
crit_err_intercept(); /* Trap critical errors */
setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */
old_int8 = (intfuncp)getvect(0x08);
setvect(0x08, int8_handler);
asm cli;
outportb(0x43, 0x36);
outportb(0x40, tickdiv & 0xFF);
outportb(0x40, tickdiv >> 8);
asm sti;
while ((ch = fgetc(infile)) != EOF)
if (ch < 0x80)
playmorse(morsecode[ch]);
putmorse(1); /* Make sure the speaker is off */
while (inptr != outptr)
; /* Wait for buffer to empty */
setvect(0x08, old_int8);
old_int8 = (intfuncp)0xFFFFFFFFL;
restore_normal();
exit(0);
}
-------------------------------- snip snip snip --------------------------------
## 8.6 DYNAMIC FAST TICK PERIODS
In the Morse code player sample program, once the new fast tick rate has been
chosen and programmed, it is not modified until the program terminates. The
interrupt keeps occurring regularly at the fast tick rate. However, it is
possible to dynamically change the fast tick rate on a per-interrupt basis.
There are several reasons why you might want to do this - I can think of
four applications, there may be more:
■ You might want to create a signal with an uneven or completely
arbitrary duty cycle, such as 5 ms high, 40 ms low (this example
could also be done using a constant fast tick at 5 ms intervals
and counting eight interrupts to get the 40 ms delay),
■ You might be using the timer interrupt to schedule things which
happen at irregular intervals, with some long gaps, some short,
■ You might want your background interrupt routine to be able to
adjust its speed according to user actions, such as keypresses
which control the program 'speed',
■ You might want an exact number of interrupts per second, which
is not possible with a fixed divisor - see section »» 8.7 for
a sample program that does this.
All of these requirements can be handled in the same way. The technique
involves the interrupt routine adjusting the value in the Reload register
according to its requirements, to adjust the period between interrupts in a
dynamic fashion.
When the fast timer tick interrupt handler reprograms the Reload register, the
new Reload register value does not affect the current countdown in progress,
i.e. the length of time until the next interrupt, it affects the length of
time between the next interrupt and the interrupt after that. In other words,
you could say there is a one interrupt delay before the new value takes effect.
## 8.7 SAMPLE PROGRAM: DYNAMIC FAST TICK INTERRUPT HANDLER
This sample program gives a fast tick rate of exactly 1000 fast ticks per
second, using an effective divisor of 1193.18166666....
This cannot be achieved with a static divisor - the closest static divisors
of 1193 and 1194 produce 1000.152277 and 999.3146287 interrupts per second
respectively. To get exactly 1000 fast ticks per second, the divisor must
be changed dynamically to give an effective divisor of 1193.181666... by
cycling through the appropriate sequence of 1193 and 1194 divisors. Over a
short period of time, the tick rate will rapidly approach exactly 1000 ticks
per second (ignoring the error due to crystal inaccuracies, etc).
The sequence of divisors is determined as follows. For 1000 ticks per second
the divisor is 1193.18166666... which is 1193 plus 9/50 (0.18) plus 1/600
(0.0016666...), which is also 1193 plus 1/5 minus 1/50, plus 1/600.
Count every fifth interrupt. On four out of every five interrupts, use a
divisor of 1193, but on every fifth interrupt, when the divide-by-five counter
carries, prepare to use 1194, and count a divide by 10 counter (which is really
dividing by 50). If the counter _doesn't_ carry, use 1194. These two counters
in combination add the 9/50. If the divide by 10 counter carries, prepare to
use 1193, and count down a divide by 12 counter, which is actually counting
1/600ths; if it carries, use 1194.
A similar approach could be used to get 200 fast tick interrupts per second
(i.e. a 5ms fast tick interval). The divisor is 5965.90833333333, which is
5965 plus 9/10 plus 1/120, so you would use 5966 for 9 of every 10 cycles and
use 5965 on the tenth, except if it is the twelfth tenth cycle in which case
use 5966.
Mode two must be used for this technique. See the description of behaviour
with odd divisors in section »» 7.8.5 for the reasons.
See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-------------------------------- snip snip snip --------------------------------
/*
Sample program #14
Demonstrates dynamic timer tick rates
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save and assemble the critical error module CRIT_ERR
Save this sample code to SAMPLE14.C
Compile this module with:
bcc -c -I<inc_path> -ms sample14.c
Link the modules with:
tlink /c /x <c0_path>\c0s.obj sample14.obj crit_err.obj,
sample14, nul, <lib_path>\cs
Where inc_path is the path to your C header files, c0_path is the path to your
startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, and cli */
#include <bios.h> /* Needed for bioskey() */
#include <dos.h> /* Needed for MK_FP() */
#include <io.h> /* Needed for _write() */
#include <stdio.h> /* Needed for printf() */
#include <stdlib.h> /* Needed for exit() */
#define FALSE 0
#define TRUE 1
#define STDERR 2 /* DOS handle for standard error */
#define BASETICK 1193
void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */
unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */
typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */
intfuncp old_int8 = (intfuncp)0xFFFFFFFFL;
static volatile unsigned long milliseconds = 0; /* Milliseconds counter */
/* The interrupt handler is responsible for updating the tick divisor to give
exactly 1000 ticks per second. It also increments a 32-bit counter which
is used by the mainline. */
void interrupt int8_handler(void) {
static unsigned int div_5 = 2;
static unsigned int div_5_10 = 5;
static unsigned int div_5_10_12 = 6;
static unsigned int int8sched = 0;
static unsigned int fastdiv = 0;
asm {
mov ax,1193 /* Prepare to use 1193 */
dec [div_5] /* Count down divide by 5 */
jns GotNewDiv /* If not reached one fifth yet */
mov [div_5],4 /* Reset dividing register */
inc ax /* Prepare to use 1194 */
dec [div_5_10] /* Count down nested divide by 10 */
jns GotNewDiv /* If not reached 1/10 of 1/5 yet */
mov [div_5_10],9 /* Reset dividing register */
dec ax /* Prepare to use 1193 */
dec [div_5_10_12] /* Count down nested divide by 12 */
jns GotNewDiv /* If not reached 1/12 of 1/10 of 1/5 */
inc ax /* The 1/600th! */
mov [div_5_10_12],11 /* Reset dividing register */
}
GotNewDiv:
asm {
cmp ax,[fastdiv] /* Got divisor in AX - did it change? */
je SameDiv /* If not, don't reprogram CTC 0 */
mov [fastdiv],ax /* Store new value */
out 40h,al /* Write new lobyte */
mov al,ah /* Get hibyte */
out 40h,al /* Write new hibyte */
}
SameDiv: /* End of inline assembly */
++milliseconds; /* Increment millisecond count */
int8sched += fastdiv;
if (int8sched < fastdiv) { /* If carried */
(old_int8)(); /* Chain to BIOS */
}
else
/** note - may not support Microchannel machines */
outportb(0x20, 0x20); /* Send EOI if not chaining */
return; /* From interrupt */
}
void restore_normal(void) {
asm pushf;
asm cli;
outportb(0x43, 0x36);
outportb(0x40, 0);
outportb(0x40, 0); /* Restore normal divisor */
asm popf;
return;
}
void abort_cleanup(int dos_is_safe) {
if (dos_is_safe) {
if (old_int8 != (intfuncp)0xFFFFFFFFL) {
setvect(0x08, old_int8);
old_int8 = (intfuncp)0xFFFFFFFFL;
}
}
else {
if (old_int8 != (intfuncp)0xFFFFFFFFL) {
*((intfuncp far *)MK_FP(0, 0x08 << 2)) = old_int8;
old_int8 = (intfuncp)0xFFFFFFFFL;
}
}
restore_normal();
return;
}
void interrupt ctrl_c_handler(void) {
static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n";
if (is_at_crit_prompt())
abort_cleanup(FALSE);
else {
abort_cleanup(TRUE);
_write(STDERR, &message, sizeof(message));
}
exit(255);
}
void poll_exit(void) {
if (bioskey(1)) {
if ((bioskey(0) & 0xFF) == 27) {
setvect(0x08, old_int8);
old_int8 = (intfuncp)0xFFFFFFFFL;
restore_normal();
exit(0);
}
}
return;
}
void main(void) {
unsigned long ms;
printf("Sample program #14 - Millisecond timer demonstrating dynamic timer tick\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
printf("Press <Esc> to exit\n\n");
crit_err_intercept(); /* Trap critical errors */
setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */
old_int8 = (intfuncp)getvect(0x08);
setvect(0x08, int8_handler);
asm cli;
outportb(0x43, 0x34); /* Must use mode two! */
outportb(0x40, BASETICK & 0xFF);
outportb(0x40, BASETICK >> 8);
asm sti;
while (1) {
asm cli;
ms = milliseconds;
asm sti;
printf("%010ld ms\r", ms);
poll_exit();
}
}
-------------------------------- snip snip snip --------------------------------
Note the order in which things are restored to normal in poll_exit(). The call
to restore_normal() to set the tick rate back to normal, must appear after the
fast tick handler has been disconnected. If the fast tick handler was still
connected after the tick rate was set to normal, it could reprogram the CTC
again, and the program would terminate with the tick running with a divisor of
1193 or 1194 (at roughly 1ms intervals)!
This program could be used to measure the execution time of another program
with 1ms resolution, provided that the other program did not use CTC channel
zero itself, and provided that the other program did not lock interrupts out
for more than 1ms at a time.
## 9 READING AN ABSOLUTE TIMESTAMP
It is possible to read an absolute timestamp at any moment in time. This
timestamp is comprised of the value read from the Counter Latch register in
channel 0 of the CTC (see sections »» 7.14 and »» 7.15), which is 16 bits
wide, and the BIOS Tick Count variable (see section »» 4) which is 32 bits
wide, though only the bottom 21 bits are used.
Mode two is easiest to use for this purpose. BIOSes traditionally set up CTC
channel zero to run in mode three, but recent BIOSes seem to be using mode two.
See section »» 7.4.2 for details.
In order to be able to read an absolute timestamp, your program must first
ensure that CTC channel zero is operating in mode two with a divisor of 65536
and that the lobyte/hibyte flag is in sync. This is most easily ensured by
simply setting the mode and divisor in the initialisation section of your
program. See section »» 7.10 and section »» 7.12 for details.
Reading the count in progress is described in sections »» 7.15, »» 7.15.1, and
»» 7.16. Reading the BIOS tick count variable is described in sections »» 4.5
and »» 4.6.
To ensure that a correct value is read, it is necessary to read the BIOS tick
count first, then read the count in progress in the CTC, then enable interrupts,
then re-read the BIOS tick count, then work out whether the first or second BIOS
tick count value is appropriate (if they are different). This is demonstrated
in the sample program in section »» 9.1.
## 9.1 SAMPLE PROGRAM: ABSOLUTE TIME REFERENCE (TIMESTAMP) IN MODE TWO
This program demonstrates the initialisation required to set the timer to run
in mode two, and a function that will return an absolute timestamp, in units
of 0.8381 microseconds since midnight on the current day. Initially it will
display the timestamp every time a key is pressed. Once the <Esc> key is
pressed, it goes into continuous timestamp checking mode, where it continuously
requests and displays the absolute timestamp, and also checks that the
timestamp never goes backwards. If the timestamp goes backwards, it displays
the two timestamp values before the error, and the first timestamp after the
negative increment. This will normally occur only at midnight.
Pressing <Esc> again will terminate the program.
See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-------------------------------- snip snip snip --------------------------------
/*
Sample program #15
Demonstrates absolute timestamping in mode two
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save this file to SAMPLE15.C and compile with:
bcc -I<inc_path> -L<lib_path> -ms sample15.c
Where inc_path is the path to your C header files and your startup modules
C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, cli, and sti */
#include <bios.h> /* Needed for bioskey() */
#include <stdio.h> /* Needed for printf() */
#include <stdlib.h> /* Needed for exit() */
#define FALSE 0
#define TRUE 1
typedef struct {
unsigned int part;
unsigned long ticks;
} timestamp;
#define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL)
void set_mode2(void) {
auto unsigned int tick_loword;
tick_loword = * BIOS_TICK_COUNT_P;
while ((unsigned int) * BIOS_TICK_COUNT_P == tick_loword)
;
asm pushf;
asm cli;
outportb(0x43, 0x34); /* Channel 0, mode 2 */
outportb(0x40, 0x00); /* Loword of divisor */
outportb(0x40, 0x00); /* Hiword of divisor */
asm popf;
return;
}
void get_timestamp(timestamp * tsp) {
auto unsigned long tickcount1, tickcount2;
auto unsigned int ctcvalue;
auto unsigned char ctclow, ctchigh;
asm pushf;
asm cli;
tickcount1 = * BIOS_TICK_COUNT_P;
outportb(0x43, 0); /* Latch value */
ctclow = inportb(0x40);
ctchigh = inportb(0x40); /* Read count in progress */
asm sti; /* Force interrupt ENABLE */
ctcvalue = - ((ctchigh << 8) + ctclow);
tickcount2 = * BIOS_TICK_COUNT_P;
asm popf;
if ((tickcount2 != tickcount1) && (ctcvalue & 0x8000))
tsp->ticks = tickcount1;
else
tsp->ticks = tickcount2;
tsp->part = ctcvalue;
return;
}
void main(void) {
auto timestamp ts, ts1, ts2;
auto unsigned int sched;
auto unsigned int ch;
printf("Sample program #15 - Demonstrates absolute timestamping\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
printf("Press any key to get timestamp; press <Esc> for continuous test\n\n");
set_mode2();
do {
ch = bioskey(0); /* Get a keypress */
get_timestamp(&ts); /* Get timestamp */
printf("Absolute timestamp: 0x%04X%04X%04X units of 0.8381 us\n",
(unsigned int) (ts.ticks >> 16),
(unsigned int) (ts.ticks & 0xFFFF),
ts.part);
} while ((ch & 0xFF) != 27);
printf("\nProgram is now performing continuous timestamp test\n\n");
printf("Press <Esc> to exit\n\n");
while (1) {
ts2.ticks = ts1.ticks;
ts2.part = ts1.part;
ts1.ticks = ts.ticks;
ts1.part = ts.part;
get_timestamp(&ts);
printf("0x%04X%04X%04X\r",
(unsigned int) (ts.ticks >> 16),
(unsigned int) (ts.ticks & 0xFFFF),
ts.part);
if ((ts.ticks < ts1.ticks) || ((ts.ticks == ts1.ticks) &&
(ts.part < ts1.part))) { /* Went backwards? */
printf("Timestamp went backwards: 0x%04X%04X%04X, 0x%04X%04X%04X, then 0x%04X%04X%04X\n",
(unsigned int) (ts2.ticks >> 16),
(unsigned int) (ts2.ticks & 0xFFFF),
ts2.part,
(unsigned int) (ts1.ticks >> 16),
(unsigned int) (ts1.ticks & 0xFFFF),
ts1.part,
(unsigned int) (ts.ticks >> 16),
(unsigned int) (ts.ticks & 0xFFFF),
ts.part);
}
++sched;
if (!(sched & 0xFF))
if (bioskey(1))
if ((bioskey(0) & 0xFF) == 27)
break;
}
exit(0);
}
-------------------------------- snip snip snip --------------------------------
The interrupt flag is carefully controlled inside the get_timestamp() function.
Interrupts must remain enabled during normal execution of the program, so that
the tick interrupt can maintain the BIOS tick count variable which forms part
of the timestamp value.
The state of the interrupt flag on entry to get_timestamp() is not important,
but the function will enable interrupts during its operation.
This program can be modified to support mode 3 operation of CTC channel zero
but this is not necessary as there are no disadvantages to operating the CTC
in mode two.
{JAM} says that this technique gives an accurate timestamp with a resolution
of few microseconds. On the computer he used, an Epson 20MH 386/SX, "Reasonable
clock code is accurate to about 4 microseconds with a minimum read time of about
20 microseconds. The clock accuracy does not change much between machines and
is never under 1 microseconds or over 4".
{JAM} also points out that timer reads take in the region of three to eight
CTC clock periods and therefore you cannot just wait for a particular time
value to occur, because you probably will not sample the count at exactly the
right time. You have to check for _at least_ that length of time elapsed.
Finally, because the absolute timestamp value ranges from 0x000000000000 to
0x001800AFFFFF then wraps around to midnight, subtracting two timestsamp
values will not give a correct indication of elapsed time if the period
measured crossed a midnight boundary. See section »» 9.3 for details.
## 9.2 SAMPLE PROGRAM: ABSOLUTE TIMESTAMP IN MODE TWO - ASSEMBLER
This program implements the second section of the sample program from section
»» 9.1 but is written in assembler and performs direct screen writes. The
get_timestamp() function is much more carefully optimised. To get an idea of
how often this program can read the timer, update the number in screen memory,
and perform several other tasks, set your system time to 23:59:50 and run the
program, and note how far apart the three reported numbers are. On my
486DX2-66, they are mostly between 16 and 32 CTC clocks, and the GetTimestamp
function takes between 7 and 9 CTC clocks to read its timestamp.
For maximum speed, this program uses the BIOS Ctrl-Break flag at 0000:0471 to
allow the program to be terminated, so you must press Ctrl-Break to terminate
the program.
-------------------------------- snip snip snip --------------------------------
NAME SAMPLE16
; Sample program #16
; Demonstrates absolute timestamping using mode 2, in assembler
; Part of the PC Timing FAQ / Application notes
; By K. Heidenstrom (kheidens@actrix.gen.nz)
;
; This program assembles into SAMPLE16.COM, a small program which sets CTC
; channel 0 to mode 2 and repeatedly reads an absolute timestamp using the
; BIOS tick count variable and the count in progress in CTC channel 2, and
; displays the 48-bit timestamp (37 bits of which are actually used) as a
; 12-digit hex number in the bottom left hand corner of the screen. It also
; checks for the timestamp going backwards, and if this occurs, displays a
; message giving the two timestamps prior to the timestamp going backwards,
; and the timestamp on which the error was detected. If midnight passes,
; this message should be displayed, as the timestamp is only an offset into
; the current day.
;
; This program assumes it is running in text mode. It supports colour and
; monochrome systems and 43-line and 50-line modes.
;
; Save this file to SAMPLE16.ASM and assemble with:
; masm sample16;
; link sample16;
; exe2bin sample16.exe sample16.com
; or
; tasm sample16;
; tlink /t sample16;
;
ComFile SEGMENT
ASSUME cs:ComFile,ds:ComFile,es:nothing,ss:nothing
ORG 100h ; Com-type file
Main PROC near
jmp Main2 ; Skip
Main ENDP
InitialMsg DB 13,44 DUP(10)
DB "Sample program #16 - Demonstrates absolute timestamping in mode 2",13,10
DB "Part of the PC Timing FAQ / Application notes",13,10
DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10,13,10
DB "Press Ctrl-Break to terminate program",13,10,13,10,"$"
BackwardsMsg DB 13,"Timestamp went backwards: 0x"
Backwards1 DB "xxxxxxxxxxxx, 0x"
Backwards2 DB "xxxxxxxxxxxx, then 0x"
Backwards3 DB "xxxxxxxxxxxx",13,10,13,10,"$"
HexBuffer DB "xxxxxxxxxxxx"
TimeL DW 0 ; Time loword (CTC count)
TimeM DW 0 ; Time midword (loword of tick count)
TimeH DW 0 ; Time hiword (hiword of tick count)
Time1L DW 0 ; Old time loword
Time1M DW 0 ; Old time midword
Time1H DW 0 ; Old time rock 'n' roll
Time2L DW 0 ; Old old time loword
Time2M DW 0 ; Old old time midword
Time2H DW 0 ; Old old time hiword
RegenSeg DW 0B800h ; Regen buffer segment
BotLine DW 0 ; Regen offset of bottom line
Main2 PROC near
cld
xor ax,ax ; Zero
mov es,ax
cmp WORD PTR es:[463h],3D4h ; Check for colour mode
je GotRegenSeg
mov RegenSeg,0B000h
GotRegenSeg: xchg ax,bx ; BX = 0 (page number)
mov ah,3
int 10h ; Get cursor position - DH = line
mov ah,0Fh
int 10h ; Get video mode
mov al,ah ; Get screen width
mul dh ; Calculate offset of bottom line
shl ax,1 ; Shift for char/attrib
mov BotLine,ax ; Store
mov dx,OFFSET InitialMsg
mov ah,9
int 21h ; Display initial message
call InitCTC0Mode2 ; Init CTC chan 0 to mode 2, reload 0
MainLoop:
mov ax,Time1L ; Copy Time1 to Time2
mov Time2L,ax
mov ax,Time1M
mov Time2M,ax
mov ax,Time1H
mov Time2H,ax
mov ax,TimeL ; Copy Time to Time1
mov Time1L,ax
mov ax,TimeM
mov Time1M,ax
mov ax,TimeH
mov Time1H,ax
call GetTimestamp ; Get timestamp
mov TimeL,ax ; Store it
mov TimeM,dx
mov TimeH,bx
sub ax,Time1L ; Subtract lowords
sbb dx,Time1M ; Subtract midwords with borrow
sbb bx,Time1H ; Subtract hiwords with borrow
jnb Alright ; If no borrow, time didn't go backwards
mov si,OFFSET Time2L ; Oldest time
mov di,OFFSET Backwards1 ; First string position
call ToASCII ; Convert to ASCII
mov si,OFFSET Time1L ; Second-oldest time
mov di,OFFSET Backwards2 ; Second string position
call ToASCII ; Convert to ASCII
mov si,OFFSET TimeL ; New time
mov di,OFFSET Backwards3 ; Third string position
call ToASCII ; Convert to ASCII
mov dx,OFFSET BackwardsMsg
mov ah,9
int 21h ; Display went-backwards message
Alright: mov si,OFFSET TimeL ; New time
mov di,OFFSET HexBuffer ; Hex text buffer
call ToASCII ; Convert to ASCII
mov si,OFFSET HexBuffer ; ASCII hex text
mov di,BotLine ; Offset of bottom line of screen
mov es,RegenSeg ; Regen buffer segment
mov cx,12 ; Characters to copy
ScrLoop: movsb ; Copy character
inc di ; Skip attribute
loop ScrLoop ; Loop
xor ax,ax
mov es,ax
xchg al,BYTE PTR es:[471h]
test al,al
js Finish
jmp MainLoop
Finish: mov ax,4C00h
int 21h ; Terminate with errorlevel 0
int 20h ; In case DOS-1 (!)
Main2 ENDP
InitCTC0Mode2 PROC near
; Func: Initialise CTC channel 0 to operate in
; mode 2 with reload value of 0 (divisor
; of 65536, 18.2065 interrupts/second).
; Wait for a tick to occur before setting
; mode (should minimise disturbance to
; system time).
; In: None
; Out: None
; Lost: AX (preserves interrupt flag)
pushf
push ds
sti ; Ensure interrupts are enabled
xor ax,ax
mov ds,ax ; Address low memory with DS
mov ax,ds:[46Ch] ; Get loword of tick count
WaitTick: cmp ax,ds:[46Ch] ; Changed?
je WaitTick ; If not, loop
pop ds
mov al,00110100b ; Channel 0, mode 2
cli
out 43h,al ; Set mode
xor ax,ax ; Zero
jmp SHORT $+2 ; Delay
out 40h,al ; Loword of divisor
jmp SHORT $+2 ; Delay
out 40h,al ; Hiword of divisor
popf ; Restore interrupt flag
ret
InitCTC0Mode2 ENDP
PROC GetTimestamp near
; Func: Return absolute timestamp (48-bit) in
; units of 0.83809534452us since midnight
; in the current day (range 000000000000h
; to 001800AFFFFFh) using BIOS tick count
; variable and CTC channel zero count in
; progress, assuming CTC channel 0 is
; operating in mode 2 with a reload value
; of 0 (65536 divisor).
; In: None
; Out: AX = Count loword (b0..15) (0000-FFFF)
; DX = Count midword (b16..31) (0000-FFFF)
; BX = Count hiword (b32..47) (0000-0018)
; Lost: AX BX DX
; Note: This routine briefly disables then
; enables then disables interrupts
; regardless of the state of the
; interrupt flag on entry.
; It restores the original interrupt
; flag state on exit.
push ds ; Preserve register
push di
push si
pushf ; Preserve interrupt flag
xor ax,ax ; Zero
mov ds,ax ; Address low memory with DS
ASSUME ds:nothing ; Not addressing ComFile any more
cli
mov si,ds:[46Ch] ; Loword of tick count
mov di,ds:[46Eh] ; Hiword of tick count
mov al,00000000b ; Latch count for CTC channel 0
out 43h,al ; Send it
jmp SHORT $+2 ; Delay
in al,40h ; Get lobyte of count
mov ah,al ; Save in AH
jmp SHORT $+2 ; Delay
in al,40h ; Get hibyte of count
sti ; Make sure interrupts are enabled now
xchg al,ah ; Get bytes the right way round
nop ; Sniff for interrupt
neg ax ; Convert to ascending count
cli ; No interrupts again for reading count
mov dx,ds:[46Ch] ; Loword of tick count again
mov bx,ds:[46Eh] ; Hiword of tick count again
popf ; Restore original interrupt flag
cmp dx,si ; Did tick count change?
je GotTimestamp ; If not, just return second tick count
test ax,ax ; Is tick count low or high?
jns GotTimestamp ; If low, read was just past interrupt
mov dx,si ; If high, previous tick count is right
mov bx,di ; Get hiword of tick count too
GotTimestamp: pop si ; Restore working registers
pop di
pop ds ; Restore DS
ASSUME ds:ComFile ; Back to ComFile
ret
GetTimestamp ENDP
ToASCII PROC near
; Func: Convert a three-word time structure to
; 12-digit printable hex representation
; In: SI -> Structure
; DI -> ASCII buffer in this segment
; Out: DI -> Past characters stored
; Lost: AX DI ES
push cs
pop es ; ES to ComFile
mov ax,ds:[si+4] ; Get hiword
call Mach16ToHexAsc ; Convert to hex ASCII representation
mov ax,ds:[si+2] ; Get hiword
call Mach16ToHexAsc ; Convert to hex ASCII representation
mov ax,ds:[si+0] ; Get hiword
Mach16ToHexAsc PROC near
push ax
mov al,ah
call Mach8ToHexAsc
pop ax
Mach8ToHexAsc PROC near
push ax
shr al,1
shr al,1
shr al,1
shr al,1
call Mach4ToHexAsc
pop ax
and al,0Fh
Mach4ToHexAsc PROC near
add al,90h
daa
adc al,40h
daa
stosb
ret
Mach4ToHexAsc ENDP
Mach8ToHexAsc ENDP
Mach16ToHexAsc ENDP
ToASCII ENDP
ComFile ENDS
END Main
-------------------------------- snip snip snip --------------------------------
See all the comments in section »» 9.1 relating to the C program; these
comments also apply to this program.
## 9.3 HANDLING THE MIDNIGHT BOUNDARY
The absolute timestamp value returned by the functions in the above programs
will be in the range 0x000000000000 to 0x001800AFFFFF inclusive. Calculating
the time difference between two of these timestamps by subtracting the first
from the second will only give a correct result if the time period did not span
a midnight boundary. To handle this case, you must check that the second
timestamp is greater than the first, and if not, add 0x001800B00000 to the
second timestamp before subtracting them. This will give a correct result,
provided that no more than about 24 hours has elapsed between the two
timestamps being taken! (The timestamp value does not include a date).
-------------------------------- snip snip snip --------------------------------
typedef struct { /* As defined in the sample program */
unsigned int part;
unsigned long ticks;
} timestamp;
/* The following function takes two timestamps in startts and stopts, and
calculates the time difference and stores them in diffts. The difference
is in units of 0.8381 us, the same units as the timestamp values. */
void calc_elapsed(timestamp * startts, timestamp * stopts, timestamp * diffts) {
if (startts->ticks <= stopts->ticks) /* No change of day */
diffts->ticks = stopts->ticks - startts->ticks;
else /* Change of day */
diffts->ticks = stopts->ticks + 0x001800B0L - startts->ticks;
diffts->part = stopts->part - startts->part;
if (stopts->part < startts->part)
--(diffts->ticks);
return;
}
-------------------------------- snip snip snip --------------------------------
## 10 OTHER TOPICS
## 10.1 THE 586 TIME STAMP COUNTER
In a message in comp.sys.intel and comp.lang.asm.x86 in mid-December 1994,
Gordon Burditt (gordon@sneaky.lonestar.org) describes a partly undocumented
instruction available on the Intel 586 (but not guaranteed to be available on
future Intel processors). The instruction is RDTSC - Read Time Stamp Counter.
Opcode encoding is 0F 31. It is "unprivileged if bit 2 of CR4 is clear, Ring
0 or real mode only if it is set" (whatever that means :-).
This instruction loads the 64-bit Time Stamp Counter register contents into
EDX:EAX. The Time Stamp Counter is zeroed on power-up and is incremented on
each CPU clock cycle (e.g. 90 times per microsecond for a 90 MHz CPU - for
clock doubled or clock tripled processors, does this mean the external clock
or the internal clock? (*)). This level of resolution is useful for
performance measurement and CPU usage billing.
The unit of time is system-dependent, and also depends on the accuracy of the
processor clock, which may not be very good.
Use the CPUID instruction to determine if RDTSC exists on this CPU. EDX
"feature bits" bit 4 is set if it does.
The Time Stamp Counter register can be written via the documented 586
instruction WRMSR - Write Model-Specific Register, coding 0F 30. The privilege
level for this instruction is ring 0 or real mode only. Set ECX to the
register number (10 hex for the TSC register) and EDX:EAX to the new value and
execute the instruction.
Use CPUID to determine if WRMSR exists on this CPU. EDX "feature bits" bit 5
is set if it does. Also, if you are running DOS with EMM386 (i.e. V86 mode),
you cannot use the privileged instructions.
Thank you Gordon for this information.
Quoting from an article dated Apr 27 1995 in comp.lang.asm.x86 by Philip
O'Carroll (poc@maths.tcd.ie) with his permission:
>> I can't execute the RDTSC instruction... Is there someone who knows why?
>
> 1) The RDTSC instruction cannot be executed from V86 mode. It gives a
> GPF. I do not know why this is and I have only tested RDTSC from
> within protected mode.
>
> 2) If you are executing it from 16-bit code you will need to use the ADRSIZE
> prefix to access the upper 16 bits of the EAX and EDX registers.
>
> 3) It is possible that the instruction has been disabled by setting the
> TSD (timestamp disable) bit in CR4. This is unlikely because the Pentium
> powers up with it clear and I cannot see why an OS would disable it.
Terje Mathisen (Terje.Mathisen@hda.hydro.com) adds, in an article in April
1995 in comp.lang.asm.x86:
> RDTSC is by default available for all rings/modes, except V86.
>
> The V86 fault was an Intel internal error, i.e. it wasn't supposed to
> be like that.
The RDTSC instruction causes a GPF if executed in V86 mode. Terje says that
though this is the documented behaviour, according to an Intel technician the
RDTSC instruction should have worked in V86 mode too. Terje says that the
Intel technician also said at the time that RDTSC would work in V86 mode on
the P6.
> The Time Stamp Disable (TSD) bit in CR4 must be changed (set) to restrict
> RDTSC to ring 0, so (almost?) all operating systems will let you use the
> time stamps from ring 3 code.
Philip also sent me the following macro for VC++ 1.5 16-bit (protected mode
Ring 3 code):
> #define TIMESTAMP(var) __asm \
> {
> _asm emit 0x0F \
> _asm emit 0x31 \
> _asm emit 0x66 \
> _asm mov word ptr var, ax \
> _asm emit 0x66 \
> _asm mov word ptr var[+4], dx \
> }
>
> Usage:
>
> DWORD timest[2];
>
> TIMESTAMP(timestamp);
Philip also told me he has written a Windows VxD for accessing the profiling
counters from Ring 3 code, but I don't know where, or when, it will be
available.
> My VxD allows Windows apps to access the Pentium profiling registers
> detailed in Byte July 1994. Specifically there are two counters which
> can count various different processor events such as instructions
> executed, data cache hits/misses etc.
>
> The TSC _can_ be used by Windows apps without recourse to a VxD.
Thanks guys.
## 10.2 SERIAL PORT REGULAR INTERRUPT
If your application will have a spare serial port to play with, it can generate
a regular interrupt using the Transmit interrupt facility on the serial chip
(known as a UART, for Universal Asynchronous Receiver/Transmitter). There are
other ways to make the UART generate interrupts, but the Transmit interrupt is
easiest to use.
UARTs usually drive IRQ4 and IRQ3. These interrupts are reserved for COM1 and
COM2 respectively. When COM3 and COM4 are present, they sometimes 'share' IRQ4
and IRQ3 respectively, with COM1 and COM2, but this 'sharing' only works if the
ports are not used simultaneously (except on MicroChannel machines and possibly
on EISA machines, where proper interrupt sharing is possible with the right
software support). In some cases, the otherwise spare interrupt lines, such as
IRQ5 and IRQ2/9, are used for COM3 and COM4.
## 10.2.1 SERIAL PORT (UART) DOCUMENTATION
This information is brief and incomplete. There are many books and electronic
documents which describe the UART much more thoroughly, such as Chris Blum's
"The Serial Port" FAQ which is posted periodically in the Internet newsgroup
comp.os.msdos.programmer.
There are several types of UARTs. The basic device is the INS8250 which was
originally developed by National Semiconductor. It is not an Intel device,
despite the number. Descendants such as the 8250A, 16C450, and 16C550 add
features, improve performance, and/or correct design errors in previous
versions of the chip.
The UART occupies eight consecutive I/O addresses starting at the I/O Base
address. The I/O Base address of a nominated UART (e.g. COM1) can be found
in the table in the BIOS data area in low memory, starting at 0040:0000 (aka
0000:0400). The table has four entries, at 0, 2, 4, and 6, which correspond
to COM1, COM2, COM3, and COM4 respectively. If the value is zero, there is
no such port. If nonzero, it specifies the I/O Base address of that port.
The registers in the UART are as follows.
I/O address Access Name Description
----------- ------ ---- -----------
IOBase+0 Read RDR Received data (DLAB=0)
Write TDR Transmit data (write) (DLAB=0)
Read/write BRDL Divisor register lobyte (DLAB=1)
IOBase+1 Read/write IER Interrupt Enable Register (DLAB=0)
Read/write BRDH Divisor register hibyte (DLAB=1)
IOBase+2 Read-only IIR Interrupt Identification Register
Write-only FCR FIFO control register (FIFO UARTs only)
IOBase+3 Read/write LCR Line Control Register
IOBase+4 Read/write MCR Modem Control Register
IOBase+5 Read-only LSR Line Status Register
IOBase+6 Read-only MSR Modem Status Register
IOBase+7 Read/write Scratch register (on some UARTs only)
The 'DLAB' above is the Divisor Latch Access Bit, which is bit 7 of the Line
Control Register (LCR) at IOBase+3. This bit controls access to the divisor
register (hence the name). The divisor register is a 16-bit register which
acts as a divisor to determine the baud rate. It is accessible at IOBase+0
(lobyte) and IOBase+1 (hibyte) when the DLAB is set. When the DLAB is clear,
the transmit and receive data register and the IIR appear at these I/O
locations.
The relevant registers are now described briefly.
IER 7 6 5 4 3 2 1 0 IOBase+1, read/write
* * . * . . . . Not used; zero
. . * . . . . . Special function enable (some UARTs)
. . . . * . . . Modem Status Change Interrupt Enable (1=enable)
. . . . . * . . Line Status Change Interrupt Enable (1=enable)
. . . . . . * . Transmit Ready Interrupt Enable (1=enable)
. . . . . . . * Received Data Interrupt Enable (1=enable)
IIR 7 6 5 4 3 2 1 0 IOBase+2, read-only
* * . . . . . . FIFOs Enabled flags (FIFO UARTs only)
. . * * . . . . Special function status (some UARTs)
. . . . * * * . Interrupt Identification bits 2, 1, and 0
. . . . . . . * Interrupt output active (0=active, 1=inactive)
LCR 7 6 5 4 3 2 1 0 IOBase+3, read/write
* . . . . . . . Divisor Latch Access Bit (DLAB)
. * . . . . . . Set Break (1=break, 0=normal)
. . * . . . . . Stick Parity (1=stick, 0=normal parity, if enabled)
. . . * . . . . Even Parity (1=even, 0=odd, if enabled)
. . . . * . . . Parity Enable (1=enable, 0=disable)
. . . . . * . . Stop bits (1=1.5/2, 0=1 stop bits)
. . . . . . * * Word length (00=5, 01=6, 10=7, 11=8 data bits)
LSR 7 6 5 4 3 2 1 0 IOBase+5, read-only
* . . . . . . . Not used; 0
. * . . . . . . TSRE - Transmit Shift Register Empty
. . * . . . . . THRE - Transmit Holding Register Empty
. . . * . . . . BI - Break interrupt (break received)
. . . . * . . . FE - Framing Error
. . . . . * . . PE - Parity Error
. . . . . . * . OR - Overrun error
. . . . . . . * DR - Data Ready (received a data byte)
MCR 7 6 5 4 3 2 1 0 IOBase+4, read/write
* . . . . . . . Special function enable (some UARTs)
. * * . . . . . Not used; zero
. . . * . . . . Loopback enable (1=enable)
. . . . * . . . OUT2 (interrupt buffer control) (1=active)
. . . . . * . . OUT1 (1=active)
. . . . . . * . RTS - Request To Send (1=active)
. . . . . . . * DTR - Data Terminal Ready (1=active)
MSR 7 6 5 4 3 2 1 0 IOBase+6, read-only
* . . . . . . . DCD - Data Carrier Detect (0=inactive, 1=active)
. * . . . . . . RI - Ring Indicator (0=inactive, 1=active)
. . * . . . . . DSR - Data Set Ready (0=inactive, 1=active)
. . . * . . . . CTS - Clear To Send (0=inactive, 1=active)
. . . . * . . . DDCD - Delta DCD (0=no change, 1=changed)
. . . . . * . . TERI - Trailing Edge Ring Indicator (1=edge)
. . . . . . * . DDSR - Delta DSR (0=no change, 1=changed)
. . . . . . . * DCTS - Delta CTS (0=no change, 1=changed)
Bit 2 of the LCR controls the number of stop bits. If this bit is 0, one stop
bit is used. If this bit is 1, two stop bits are used, except when the word
length bits are both zero (i.e. 5-bit word length), in which case 1.5 stop bits
are used.
Bit 3 of the MCR (OUT2) controls the tristate buffer that drives the interrupt
line. When the port is in use, and the interrupt facility is required, this
bit must be set, to enable the buffer to drive the IRQ line on the slot bus.
The baud rate divisor is chosen as 115200 divided by the baud rate. For
example if a baud rate of 9600 bits per second is required, the divisor value
is 115200/9600, which is 12. Both lobyte and hibyte must be programmed. The
DLAB must be set prior to writing the divisor, and turned off afterwards.
For a ten-bit character length (e.g. 8-bit data with no parity, or 7-bit data
with parity), the transmitter will generate a transmit ready interrupt ten
times slower than the bit rate, e.g. 960 times per second in the above example.
The serial port interrupt must be enabled on the PIC for interrupt driven
operation (see section »» 6.10 for details).
There are four independently controllable interrupt sources in the UART. They
correspond to bits 3-0 of the IER. When handling interrupts from the UART when
more than one interrupt source is enabled in the IER, particularly if the modem
status change interrupt is enabled, your software must take care to ensure that
all interrupt sources are acknowledged before sending the EOI command to the
PIC. This condition can be detected by checking bit 0 of the Interrupt
Identification Register (IIR) - if this bit is zero, then an unacknowledged
interrupt source is still pending. This will be one of the interrupt sources
that are enabled via the IER. This condition must be cleared before the EOI
is sent.
The Received Data interrupt is cleared when the character is read from the
Received Data register. The Transmit Ready interrupt is cleared when any
character is written to the Transmit Data register. The Line Status Change
and Modem Status Change interrupts are cleared by a read of the LSR and the
MSR, respectively.
The program in section »» 10.2.2 demonstrates how to use a serial port as a
regular interrupt source.
## 10.2.2 SAMPLE PROGRAM: REGULAR INTERRUPT USING THE SERIAL PORT
This program uses a serial port (COM1 in this case) to generate a regular
(periodic) interrupt. The UART divisor is set to 96, giving a baud rate of
1200 bps. At ten bits per character, the UART will transmit a character 120
times per second, and generate the Transmit Ready interrupt at the same rate.
This program has the IRQ and interrupt numbers, and the serial port's I/O Base
address, hard-coded via #defines. These could be set by command line options
and/or determined via the table of addresses at 0000:0400 described earlier.
Note that while this program is running, it will be transmitting characters
out the serial port at 1200 baud. If a serial printer, or any other device,
is connected to the serial port, you might want to remove it before running
this program!
See section »» 10.2.3 for a method of incorporating this timing technique into
a program that is already using the serial port, to implement delays in a
transmitted data stream.
See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-------------------------------- snip snip snip --------------------------------
/*
Sample program #17
Demonstrates regular interrupts using the serial port
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save and assemble the critical error module CRIT_ERR
Save this sample code to SAMPLE17.C
Compile this module with:
bcc -c -I<inc_path> -ms sample17.c
Link the modules with:
tlink /c /x <c0_path>\c0s.obj sample17.obj crit_err.obj,
sample17, nul, <lib_path>\cs
Where inc_path is the path to your C header files, c0_path is the path to your
startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, and cli */
#include <bios.h> /* Needed for bioskey() */
#include <dos.h> /* Needed for MK_FP */
#include <io.h> /* Needed for _write() */
#include <stdio.h> /* Needed for printf() */
#include <stdlib.h> /* Needed for exit() */
#define FALSE 0
#define TRUE 1
#define STDERR 2 /* DOS handle for standard error */
#define BAUDDIV 96 /* Interrupt rate = 11520 / BAUDDIV */
#define IOBASE 0x3F8 /* COM1 standard I/O base address */
#define COMIRQ 4 /* IRQ number for COM1 (standard) */
#define COMINT 0x0C /* Corresponding interrupt number */
#define PICMASK (1 << COMIRQ) /* Bitmask for interrupt in PIC IMR */
void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */
unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */
typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */
intfuncp old_com_int = (intfuncp)0xFFFFFFFFL;
static unsigned int onetwentieths = 0; /* 120ths of seconds */
static unsigned int seconds = 0; /* Seconds */
static unsigned char old_brdl, old_brdh; /* Old baud rate divisor */
static unsigned char old_lcr, old_mcr, old_ier; /* Old LCR, MCR, IER contents */
/* The interrupt handler is invoked when the UART is transmit ready. It must
feed the UART to shut it up. When the UART is hungry again, it will issue
another interrupt. This handler increments a counter variable. */
void interrupt com_int_handler(void) {
outportb(IOBASE, 0x00); /* "Feeeed me Seymour" */
if (++onetwentieths >= 120) { /* Increment 120ths count */
onetwentieths = 0;
++seconds;
}
outportb(0x20, 0x20); /* Send EOI */
return; /* From interrupt */
}
void restore_normal(void) {
asm pushf;
asm cli;
outportb(0x21, inportb(0x21) | PICMASK); /* Disable int in PIC */
outportb(IOBASE + 3, old_lcr & 0x7F); /* Clear DLAB */
outportb(IOBASE + 1, old_ier); /* Restore IER */
outportb(IOBASE + 3, old_lcr | 0x80); /* Set DLAB */
outportb(IOBASE + 0, old_brdl); /* Lobyte of divisor */
outportb(IOBASE + 1, old_brdh); /* Hibyte of divisor */
outportb(IOBASE + 3, old_lcr); /* Restore LCR */
outportb(IOBASE + 4, old_mcr); /* Restore MCR */
asm popf;
return;
}
void abort_cleanup(int dos_is_safe) {
if (dos_is_safe) {
if (old_com_int != (intfuncp)0xFFFFFFFFL) {
setvect(COMINT, old_com_int);
old_com_int = (void far *)0xFFFFFFFFL;
}
}
else {
if (old_com_int != (intfuncp)0xFFFFFFFFL) {
*((intfuncp far *)MK_FP(0, COMINT << 2)) = old_com_int;
old_com_int = (void far *)0xFFFFFFFFL;
}
}
restore_normal();
return;
}
void interrupt ctrl_c_handler(void) {
static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n";
if (is_at_crit_prompt())
abort_cleanup(FALSE);
else {
abort_cleanup(TRUE);
_write(STDERR, &message, sizeof(message));
}
exit(255);
}
void poll_exit(void) {
if (bioskey(1)) {
if ((bioskey(0) & 0xFF) == 27) {
setvect(COMINT, old_com_int);
old_com_int = (void far *)0xFFFFFFFFL;
restore_normal();
exit(0);
}
}
return;
}
void main(void) {
unsigned int main_onetwentieths, main_seconds, old_onetwentieths;
printf("Sample program #17 - Demonstrates regular interrupts using the serial port\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
printf("Press <Esc> to exit\n\n");
crit_err_intercept(); /* Trap critical errors */
setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */
old_com_int = getvect(COMINT);
setvect(COMINT, com_int_handler);
asm cli;
old_lcr = inportb(IOBASE + 3); /* Get old LCR value */
old_mcr = inportb(IOBASE + 4); /* Get old MCR value */
outportb(IOBASE + 3, 0x83); /* Set DLAB */
old_brdl = inportb(IOBASE + 0); /* Get divisor lobyte */
old_brdh = inportb(IOBASE + 1); /* Get divisor hibyte */
outportb(IOBASE + 0, BAUDDIV & 0xFF); /* Set up divisor lobyte */
outportb(IOBASE + 1, BAUDDIV >> 8); /* Set up divisor hibyte */
outportb(IOBASE + 3, 0x03); /* Clear DLAB */
old_ier = inportb(IOBASE + 1); /* Get old IER value */
outportb(IOBASE + 4, 0x08); /* Enable interrupt buffer */
/* Use 0x18 instead of 0x08 above to set loopback mode so
data is not transmitted out the serial port connector */
outportb(IOBASE + 1, 0x00); /* No interrupts yet */
outportb(0x21, inportb(0x21) & (~PICMASK)); /* Enable int in PIC */
outportb(IOBASE + 1, 0x02); /* Enable Tx interrupt */
asm sti;
printf("Seconds 120ths\n");
while (1) {
asm cli;
main_onetwentieths = onetwentieths;
main_seconds = seconds;
asm sti;
if (main_onetwentieths != old_onetwentieths) {
printf("%5d %3d\r",
main_seconds, main_onetwentieths);
old_onetwentieths = main_onetwentieths;
}
poll_exit();
}
}
-------------------------------- snip snip snip --------------------------------
## 10.2.3 INSERTING DELAYS INTO SERIAL PORT TRANSMITTED DATA
The information and code given in this section is untested.
In controller applications, it is sometimes necessary to insert delays into a
serial transmission. This may be required as part of a communication protocol,
or for other reasons. For example, certain types of modems used in medium
speed data communication cannot accept data to be transmitted immediately when
the transmit enable flow control line is driven active by the computer, so the
computer must raise flow control and delay for a certain time (usually in the
order of 5-20 milliseconds) before starting to transmit data.
These delays can be created using the transmit interrupt method, using the same
serial port which transmits the data, via the loopback enable bit, bit 4 of the
MCR (see section »» 10.2.1). Setting this bit forces the UART's data output in
the idle (marking) state, and loops its transmitted data back to its receiver,
internally to the UART chip. In this state, the transmit ready interrupt can be
used to time the delay period as per the sample program in section »» 10.2.2.
When the required number of interrupts have occurred, i.e. the required delay
time has elapsed, wait for the last byte to be serialised (by waiting for TSRE
in the LSR to go true) and then turn off loopback mode. Your program can then
begin transmitting.
This method cannot be used if your program must be able to receive characters
during the delay period, because in loopback mode, the UART ignores the receive
data signal, but this method can be used in half duplex applications. Also,
the granularity of the delay is one character length. Reprogramming the baud
rate during the delay period might allow finer delay timing, but this would be
very technical, if not impossible, to implement correctly.
If your program transmits under interrupt, I would suggest using some flags to
communicate between the mainline and the interrupt handler. For example, the
mainline could signal the start of a transmission by enabling outgoing flow
control, selecting loopback mode, sending one or two characters to the UART,
setting an 'idle-leader' flag to be used by the interrupt routine, and enabling
transmit interrupts. The interrupt routine would check the interrupt source
(if more than one source is enabled on the UART) and if the interrupt is due to
transmit ready, first check whether the idle-leader flag is set, and if so,
send any character to the port (e.g. 0FF hex), decrement the idle leader
counter, and return. If the idle leader timer counts down to zero, either the
mainline or the interrupt routine would have to wait for TSRE to go active,
turn off loopback mode, and start transmitting data.
If your program is doing nothing while it waits during the transmit idle period,
and does not otherwise transmit under interrupt, you can use the transmit ready
interrupt signal without actual interrupts, in a polled fashion. Fast response
to a transmit ready signal is not necessary, as there is a window of about two
character lengths between when the THRE (Transmit Holding Register Empty) signal
goes true, and when the transmit data register must be filled, due to the double
buffering provided by the transmit holding register and transmit shift register.
Here is a crude, untested function to transmit a string of data bytes without
using interrupts. It asserts outgoing flow control (DTR and RTS), and waits
for a number of character-periods determined by the leader_len parameter, then
transmits the message pointed to by the msg parameter for the number of bytes
specified by the msg_length parameter, waits for the last character to be
fully serialised plus nearly one character length, and drops the RTS line.
A similar method can be used with an interrupt handler, with quite a lot of
extra mucking around.
void wait_tx_string(unsigned leader_len, char * msg, unsigned msg_length) {
while ((inportb(IOBASE+5) & 0x60) != 0x60)
; /* Wait for last char to be serialised */
asm pushf;
asm cli;
outportb(IOBASE+4, inportb(IOBASE+4) | 0x13); /* DTR, RTS, loopback */
asm popf;
while (leader_len--) {
outportb(IOBASE, 0xFF); /* Dummy byte */
while ((inportb(IOBASE+5) & 0x20) == 0)
; /* Wait for THRE again */
}
while ((inportb(IOBASE+5) & 0x40) == 0)
; /* Wait for TSRE */
asm pushf;
asm cli;
outportb(IOBASE+4, inportb(IOBASE+4) & 0xEF); /* Loopback off */
asm popf;
while (msg_length--) {
outportb(IOBASE, *(msg++));
while ((inportb(IOBASE+5) & 0x20) == 0)
; /* Wait for THRE between chars */
}
while ((inportb(IOBASE+5) & 0x40) == 0)
; /* Wait for last char sent */
asm pushf;
asm cli;
outportb(IOBASE+4, inportb(IOBASE+4) | 0x10); /* Loopback back on */
asm popf;
outportb(IOBASE, 0xFF); /* Dummy byte */
while ((inportb(IOBASE+5) & 0x60) != 0x60)
; /* Wait for dummy char to be serialised */
asm pushf;
asm cli;
outportb(IOBASE+4, inportb(IOBASE+4) & 0xED); /* RTS, loopback off */
asm popf;
return;
}
This is only an outline of this technique. If you are implementing this system,
I would strongly recommend a thorough read of a technical document on the serial
port, such as Chris Blum's article (see section »» 10.2.1) or manufacturers'
data sheets for the serial chips, so you can determine all the implications of
your code's actions. This is particularly important if timing is very critical,
as there are timing subtleties and interactions between the transmit holding
register and the transmit shift register that must be taken into account.
There is also a problem caused by the fact that a transmit ready interrupt is
acknowledged by a read of the LSR. This has serious implications relating to
when the LSR may be interrogated. If the mainline accesses the LSR, it may
clear a pending interrupt condition, causing transmit interrupts to cease.
I have not investigated this properly, but be warned! (*)
## 10.3 EXTERNAL INTERRUPT SOURCES
An external interrupt source can be used for many things, including timekeeping.
External hardware of some sort will normally be required to drive the interrupt
in the desired way. Usually the external interrupt source will use the parallel
port or the serial port to get access to an interrupt level (IRQ) on the slot
bus.
The parallel or serial port input can be driven by an external source at the
desired rate. If only a slow interrupt rate is required, you can clock the
input at 300 Hz, which can be derived using a PLL (Phase Locked Loop) from the
mains frequency. 300 Hz is a good choice because it can be generated from both
50Hz (Europe) and 60Hz (America) mains frequencies. Thanks to John Stockton
for suggesting this technique (though he points out that he has not tested it).
You may have noticed how you never have to adjust clocks that are mains powered
(except after power loss, of course). This is because the mains frequency is
usually regulated very carefully by power supply authorities and, though it may
vary slightly in the short term, its long term accuracy should be very high.
A frequency derived from the mains in this way could be a good clock source
for timing applications which require high long-term accuracy.
## 10.3.1 EXTERNAL INTERRUPT THROUGH PARALLEL PORT
The parallel port interrupt is normally connected to IRQ7 although some cards
are jumper-selectable to IRQ5 and maybe other IRQs. The parallel port interrupt
was intended to be used in the normal course of sending data to a parallel
printer, but DOS and BIOS do not use the interrupt facility. Versions of OS/2
prior to Warp (3.0) did require the interrupt for printing, but from Warp
onwards the interrupt is not required (though it can be used if the /IRQ
switch is provided on the line in CONFIG.SYS, i.e. BASEDEV=PRINT0x.SYS /IRQ).
The basic parallel port consists of three registers at consecutive I/O
locations starting at the I/O Base address. The I/O Base address of a
nominated LPT port (e.g. LPT1) can be found in the table in the BIOS data
area in low memory, starting at 0040:0008 (aka 0000:0408). The table has
three entries, at 8, 0A, and 0C, which correspond to LPT1, LPT2, and LPT3.
If the value is zero, there is no such port. Some BIOSes may support a fourth
port base entry at 0E, but other BIOSes use this location for an unrelated
function.
The register at IOBase+2 is the Control register. Bit 4 of this register
controls the tristate buffer that drives the IRQ line, and the buffer is
enabled if the bit is set. In this state, a falling edge (high to low
transition) on the Ack signal (pin 10 of the 25-pin connector) will cause an
interrupt (providing that the interrupt is enabled in the PIC's IMR; see
section »» 6.10).
## 10.3.2 EXTERNAL INTERRUPT THROUGH SERIAL PORT
In addition to the Transmit Ready interrupt (which can provide a regular
interrupt source, see section »» 10.2 and subsections), the serial port can
issue an interrupt when received data is available, when the receiver line
status changes, and/or when the receiver 'modem status' changes. The 'modem
status' refers to the four incoming flow control lines on the serial connector
which indicate the modem status when the port is connected to a modem.
These inputs are as follows.
Name Full name Pin (9-pin) Pin (25-pin)
CTS Clear To Send 8 5
DSR Data Set Ready 6 6
RI Ring Indicator 9 22
DCD Data Carrier Detect 1 8
The modem status change interrupt is enabled by bit 3 of the Interrupt Enable
Register (IER) (see section »» 10.2.1 for details). When this interrupt is
enabled, and the interrupt buffer is enabled via the OUT2 line in the Modem
Control Register (MCR) (also see section »» 10.2.1) and the appropriate IRQ is
enabled via the IMR in the PIC (see section »» 6.10), every transition on any
of these four incoming lines will cause an interrupt request.
The current states of the four incoming lines can be read on the Modem Status
Register (see section »» 10.2.1) which also contains the 'delta' signals, which
indicate whether the corresponding line has changed state since the last time
the MSR was read. When using these signals, remember that they clear when your
program reads the MSR, so read the MSR once only, and test the delta bits in
this value - don't re-read the MSR to check for any other delta bits, as they
will all have cleared just after the MSR was read the first time.
See the notes in section »» 10.2.1 about ensuring that all interrupt sources
are acknowledged before leaving the interrupt routine.
## 10.3.3 EXTERNAL INTERRUPT THROUGH SOUND CARD
Sound cards such as the Sound Blaster can most probably generate periodic
interrupts, though these are usually used for some purpose related to sound
generation, not for timing in the general sense. I haven't investigated this
one. Get a technical reference such as the Sound Blaster Freedom project if
you want to try this.
## 10.3.4 EXTERNAL INTERRUPT THROUGH CUSTOM I/O CARD
There are many third party I/O cards that are able to generate periodic
interrupts for various purposes, and for one-off dedicated applications or for
experimenting, you may wish to use these. I have no references, but you could
try looking through advertisements in computer experimenters' magazines for
sources.
Alternatively, if you have the time, money, experience, and inclination, you
can make your own I/O card. Interrupt lines on the ISA bus are all rising edge
triggered. Just generate the rising edge, and if there is no other card driving
that line, and the interrupt is enabled in the mask register of the appropriate
PIC, the appropriate interrupt will be invoked. On ISA cards it seems to be
standard practice to drive IRQ lines with a buffer that can be put into high
impedance mode (tri-stated) or driving mode, under software control. While
this is doesn't allow for interrupt sharing, or have any other great purpose,
it is in general not a bad idea.
The EIDE, MCA, and PCI busses will be different. Get a good technical book if
you intend to try this.
## 10.4 THE JOYSTICK PORT
The joystick port, or game port, is accessed via a single I/O location, which
is normally at I/O address 201h (may be jumper-settable to 301h on some cards).
The joystick standard joystick hardware interface circuit is given in Figure 4
in the FIGURES archive. It supports four pushbutton-type inputs without
hardware debouncing, and four variable resistors (potentiometers, abbreviated
'pot') for position sensing, to support two joysticks, each with two buttons
and two pots (for the X and Y axes). Some cards support only the first
joystick (see later).
## 10.4.1 JOYSTICK PORT HARDWARE
The joystick hardware cannot generate an interrupt, and has no outputs, though
it does provide a +5V supply which can be used externally, which the parallel
port does not have. It is really only useful as a general purpose input port.
The pots are read using four independent monostable or 'one-shot' circuits.
The monostable circuits are triggered by a signal from the processor, and each
one charges or discharges a capacitor at a rate determined by the resistance of
the associated pot. When triggered, the monostable's output goes high. When
the capacitor reaches a certain voltage, the output returns low, and remains
low until the monostable is next triggered by the processor. Thus the name,
'one-shot'. The processor triggers the monostable, then measures the length of
time taken for the monostable's output to go low, to determine the resistance,
and thus the position, of the pot. The formula relating resistance to time is
supposedly: T = 24.2 + (0.011 x R) where T is the time in microseconds and R is
the resistance of the pot in ohms, but the capacitors are usually inaccurate
(+/- 20% or worse) ceramic components, and are influenced by temperature, so
the above formula is 'nominal' only. In practice the relationship will vary
from one input to the next, and depend on temperature.
The nominal pot end-to-end resistance is 100 kilohms (100000 ohms), giving a
nominal maximum timeout of about 1125 us. Times in this range can be measured
accurately using CTC channel zero or two in either mode 3 or mode 2, or using
Refresh Detect. A sample program to read the joystick position is given in
section »» 10.4.2.
The joystick connector is a 15-pin female D-sub connector. The pinout is:
Pin Dir Type Stick Button Axis Return to
1 Out +5V
2 In Btn A 1 Gnd
3 In Pot A X +5V
4 - Gnd
5 - Gnd
6 In Pot A Y +5V
7 In Btn A 2 Gnd
8 Out +5V
9 Out +5V
10 In Btn B 1 Gnd
11 In Pot B X +5V
12 - Gnd
13 In Pot B Y +5V
14 In Btn B 2 Gnd
15 Out +5V
Writing any value to the I/O port (201h or 301h) causes all four monostables to
start timing. Their outputs go high immediately, and go low a certain length
of time later, depending on the resistance of the associated potentiometer.
Reading the I/O port yields the following:
7 6 5 4 3 2 1 0
* . . . . . . . Button B2 (pin 14), 0=closed, 1=open (default)
. * . . . . . . Button B1 (pin 10), 0=closed, 1=open (default)
. . * . . . . . Button A2 (pin 7), 0=closed, 1=open (default)
. . . * . . . . Button A1 (pin 2), 0=closed, 1=open (default)
. . . . * . . . Monostable BY (from pin 13), 1=timing, 0=timed-out
. . . . . * . . Monostable BX (from pin 11), 1=timing, 0=timed-out
. . . . . . * . Monostable AY (from pin 6), 1=timing, 0=timed-out
. . . . . . . * Monostable AX (from pin 3), 1=timing, 0=timed-out
Some cards only support one joystick. You may be able to tell by looking for
a 14-pin chip with '556' in its part number (single joystick), or a 16-pin chip
with '558' in its part number (two joysticks), usually located near the 15-pin
connector. Some cards implement the joystick interface in an ASIC, in which
case you may be able to follow tracks to find how many joysticks are supported.
## 10.4.2 READING THE JOYSTICK BUTTONS AND POSITION
Most BIOSes apart from very early ones provide functions to read the buttons
and positions of the joystick, accessed via int 15h. If the function is not
supported, carry is set on return and AH may be set to 80h or 86h. Steve
McGowan and Mark Feldman in their PC-GPE article say that many machines do not
support the BIOS functions properly, and that the first function (read buttons)
may be supported, while the second function (read positions) may not.
Read Joystick Buttons : int 15h
Call with: AH = 84 hex
DX = 0000 hex
Returns: AL = Button states in bits 7-4, as read from input port
Bits 7-4 are valid in the returned value, and they default to '1' and are '0'
if the corresponding button is currently depressed. This function does not
perform any debouncing on the joystick button inputs. This means that the
bit may 'bounce' (i.e. alternate randomly, one or more times) at the instant
that it makes or breaks contact, because of the mechanical nature of the
switch.
Read Joystick Positions : int 15h
Call with: AH = 84 hex
DX = 0001 hex
Returns: AX = Joystick A, axis X (0-511, 0 if timed-out)
BX = Joystick A, axis Y (0-511, 0 if timed-out)
CX = Joystick B, axis X (0-511, 0 if timed-out)
DX = Joystick B, axis Y (0-511, 0 if timed-out)
This function reads each of the four inputs separately, disabling interrupts for
a few milliseconds each time. It may use CTC channel 0 for timing, and if so,
its calculations will be affected if CTC channel 0 is operating in a different
mode from the mode that the BIOS is expecting (e.g. if the BIOS POST set CTC
channel 0 to mode 3, and a program has subsequently reprogrammed it for mode 2,
or vice versa) or if CTC channel 0 is operating with a non-standard divisor.
Inputs which have no joystick connected will time out and be reported as zero.
## 10.4.3 NOTES FROM THE PC-GPE ARTICLE
In the joystick article in the PC Games Programmer's Encyclopedia (PC-GPE),
Steve McGowan and Mark Feldman give some useful information.
All joysticks they tested returned non-linear values, i.e. the value returned
at centre-position is not half way between the values returned at corner
positions, so most joystick setup programs require the user to set up the
centre position as well as the corner positions. (This is not surprising, as
joysticks apparently use logarithmic, not linear, potentiometers!) They suggest
a 10% 'dead zone' around the centre, as joysticks do not always centre
repeatably. Joysticks are not high quality devices, and some smoothing (e.g.
1/4 new plus 3/4 old, or 1/8 new + 7/8 old) on the position values may help.
## 10.4.4 SAMPLE PROGRAM: READING THE JOYSTICK POSITION
The following program demonstrates three methods of reading the joystick pot
positions.
The first method, ctc2_read_joystick(), uses CTC channel 2 in mode 0 for timing
the pulse produced by the joystick hardware, and also detecting timeout.
Timeout occurs when more than MAXCTCCLOCKS CTC clocks pass and the monostable
output is still active. This method reads one joystick input at a time.
The second method, refd_read_joystick(), uses the Refresh Detect signal (see
section »» 7.37) as the timing source, and reads all four inputs simultaneously.
The third method uses the BIOS function call.
The first method has two caveats: (1) the ctc2_read_joystick() function will
cut off any audio being generated using CTC channel 2, and (2) T2PORT is 0x62
on PCs and XTs with the old 8255 chip (see section »» 7.30) so if you wish to
support these machines, you will have to detect the machine type (code for this
is included in section »» 7.37.1 but is in assembler) and select the port
address accordingly. It may work under OS/2. HW_TIMER should be set ON.
The ctc2_read_joystick() function uses a similar technique to the equivalent
BIOS function, though I wrote it before I disassembled the BIOS version. It
has several advantages over the BIOS function - it doesn't rely on the mode
and divisor of CTC channel zero, so it will work if CTC channel zero has been
programmed with a different mode and/or a different divisor, it has much more
consistent and more accurate timeout detection, it reads one input at a time
(so only the relevant inputs need be read), it will often be quicker than the
BIOS function, and it has higher resolution. Its major disadvantage is that
because it uses CTC channel 2, it will stop any speaker sound that may be in
progress when the function is called (sound cards are not affected, of course).
In a practical application, the values returned from ctc2_read_joystick() could
be averaged (using a 3/4-old-averaged-value plus 1/4-new, or 7/8-old-averaged-
value plus 1/8-new, or similar algorithm, or average of last n samples) to
reduce jitter, though this will slow the response.
The second method, using Refresh Detect, will not work on a PC or XT, as they
do not have a Refresh Detect signal. Also, it assumes that the refresh rate
is as configured by the BIOS, i.e. a divisor of 18 in CTC channel 1, giving one
refresh every 15.0857 microseconds. It has a much lower resolution than the
first method, but has the advantage that it reads all four inputs at once, so
in most cases will be the quickest method, and it does not make any use of CTC
channels 0 and 2, so it does not rely on the programmed mode and divisor in
CTC channel 0, and does not disrupt speaker sound being generated via CTC
channel 2.
This sample program also reads the joystick using the BIOS function described
in the previous section, and displays the values read directly and the values
read via the BIOS.
See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-------------------------------- snip snip snip --------------------------------
/*
Sample program #18
Demonstrates three ways of reading the joystick
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save this file to SAMPLE18.C and compile with:
bcc -I<inc_path> -L<lib_path> -ms sample18.c
Where inc_path is the path to your C header files and your startup modules
C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
*/
#pragma inline; /* Required for asm pushf, popf, cli, and sti */
#include <bios.h> /* Needed for bioskey() */
#include <dos.h> /* Needed for inportb() and outportb() */
#include <stdio.h> /* Needed for printf() */
#include <stdlib.h> /* Needed for exit() */
#define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL)
#define JOYPORT 0x201 /* Joystick port I/O address */
#define MONOS 0x0F /* Bottom four bits are monostable outputs */
#define T2PORT 0x61 /* Use 0x62 for PC and XT! */
#define T2OUT 0x20 /* Bit 5 is timer 2 output readback */
#define PORTB 0x61 /* For Refresh Detect - AT only! */
#define REFDET 0x10 /* Bit 4 is Refresh Detect */
#define MAXCTCCLOCKS 1800 /* Max CTC clocks for timeout */
#define WAITCTCCLOCKS 10 /* CTC clocks for monostable recovery (<255) */
#define MAXREFRESH 100 /* Maximum refresh detect counts for timeout */
typedef struct {
int ax;
int ay;
int bx;
int by;
} joyvals;
/* The following function should preferably be called with interrupts enabled.
It preserves the state of the interrupt flag, and explicitly disables
interrupts at several places, including disabling interrupts for up to 1.5
ms during the read operation. It returns -1 if an error occurred (i.e. bad
input number specified, or timeout), otherwise it returns the number of CTC
clocks measured. It reads a single joystick pot input. */
unsigned int ctc2_read_joystick(unsigned int inputnum) {
unsigned char joymask; /* Bitmask for input */
unsigned int endtime; /* Count in timer 2 at end of pulse */
if (inputnum > 3)
return -1; /* Invalid input number */
joymask = 1 << inputnum;
asm pushf;
asm cli;
outportb(PORTB, (inportb(PORTB) & 0xFC) | 0x01); /* Enable Timer 2 */
asm popf;
if (inportb(JOYPORT) & joymask) { /* Check for still timing out */
asm pushf;
asm cli;
outportb(0x43, 0xB0); /* Chan. 2, two-byte, mode 0 */
outportb(0x42, MAXCTCCLOCKS & 0xFF);
outportb(0x42, MAXCTCCLOCKS >> 8);
while (inportb(JOYPORT) & joymask) {
if (inportb(T2PORT) & T2OUT) {
asm popf;
return -1;
}
}
asm popf;
}
asm jmp SHORT $+2
asm jmp SHORT $+2 /* Sniff for pending interrupts */
asm pushf;
asm cli;
outportb(0x43, 0x90); /* Channel 2, lobyte-only, mode 0 */
outportb(0x42, WAITCTCCLOCKS);
while ((inportb(T2PORT) & T2OUT) == 0)
; /* Wait for a short time */
asm popf;
asm jmp SHORT $+2
asm jmp SHORT $+2 /* Sniff for pending interrupts */
asm pushf;
asm cli;
outportb(0x43, 0xB0); /* Chan. 2, two-byte, mode 0 */
outportb(0x42, MAXCTCCLOCKS & 0xFF);
outportb(0x42, MAXCTCCLOCKS >> 8); /* Start channel 2 */
outportb(JOYPORT, 0); /* Start monostables */
while (inportb(JOYPORT) & joymask) {
if (inportb(T2PORT) & T2OUT) { /* Timed out */
asm popf;
return -1;
}
}
outportb(0x43, 0x80); /* Latch timer 2 */
endtime = inportb(0x42);
endtime += inportb(0x42) << 8;
asm popf;
return MAXCTCCLOCKS - endtime;
}
/* The following function should be called with interrupts enabled. It will
lock out interrupts for up to about 1.5 ms during the main timing cycle.
It reads all four joystick positions. */
void refd_read_joystick(joyvals * jv) {
unsigned char counts[16]; /* Counts per input combination */
unsigned char refcount; /* Counter for refreshes */
register unsigned char portbval; /* Value from port B, and counter */
register unsigned char inlast, inthis; /* Joystick port input values */
unsigned char timedout; /* Inputs that timed out either phase */
unsigned char changed; /* Inputs that changed */
/* Check for any monostables still timing out */
portbval = inportb(PORTB);
for (refcount = 1; refcount < MAXREFRESH; ++refcount) {
inthis = inportb(JOYPORT) & MONOS;
if (!inthis)
break; /* All monostables finished */
while (((inportb(PORTB) ^ portbval) & REFDET) == 0)
;
portbval ^= 0xFF;
}
timedout = inthis; /* Set bits for inputs that timed out */
/* Initialise counts and wait sixteen refreshes for monostables to stabilise */
for (inthis = 0; inthis < 16; ++inthis) {
counts[inthis] = 0;
while (((inportb(PORTB) ^ portbval) & REFDET) == 0)
;
portbval ^= 0xFF;
}
inlast = MONOS; /* Initialise most recent input value */
/* Timing critical stuff - could be optimised to assembly language */
asm pushf;
asm cli; /* Lock ints for timing critical stuff */
portbval = inportb(PORTB);
while (((inportb(PORTB) ^ portbval) & REFDET) == 0)
; /* Wait for refresh detect to change */
portbval ^= 0xFF;
outportb(JOYPORT, 0); /* Start the monostables */
for (refcount = 1; refcount < MAXREFRESH; ++refcount) {
inthis = inportb(JOYPORT) & MONOS;
if (inthis < inlast)
counts[inlast = inthis] = refcount;
if (!inthis)
break; /* All monostables finished */
while (((inportb(PORTB) ^ portbval) & REFDET) == 0)
; /* Wait for it to change */
portbval ^= 0xFF;
}
asm popf;
timedout |= inthis; /* Any that timed out this time */
/* Now figure out what happened */
jv->ax = jv->ay = jv->bx = jv->by = -1;
inlast = 0;
for (inthis = 0; inthis <= MONOS; ++inthis) {
if ((refcount = counts[MONOS - inthis]) != 0) {
changed = (inthis - inlast) & (timedout ^ 0xFF);
inlast = inthis;
if (changed & 1)
jv->ax = refcount;
if (changed & 2)
jv->ay = refcount;
if (changed & 4)
jv->bx = refcount;
if (changed & 8)
jv->by = refcount;
}
}
return;
}
void bios_read_joystick(joyvals * jv) {
unsigned int jax, jay, jbx, jby;
_AX = 0x8400;
_DX = 0x0001;
geninterrupt(0x15);
jax = _AX;
jay = _BX;
jbx = _CX;
jby = _DX;
jv->ax = jax;
jv->ay = jay;
jv->bx = jbx;
jv->by = jby;
return;
}
void main(void) {
joyvals refdvals, biosvals;
printf("Sample program #18 - Demonstrates reading joystick positions\n"
"Part of the PC Timing FAQ / Application notes\n"
"By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"
"Timeout (input not connected) is indicated by 65535 for the CTC2\n"
"\tand RefDet methods, and 00000 for the BIOS function method\n\n"
"Press <Esc> to exit\n\n"
"----- CTC2 method ----- ---- RefDet method ----"
" ----- BIOS method -----\n\n");
while (1) {
refd_read_joystick(&refdvals);
bios_read_joystick(&biosvals);
printf("%05u,%05u,%05u,%05u %05u,%05u,%05u,%05u %05u,%05u,%05u,%05u\r",
ctc2_read_joystick(0), ctc2_read_joystick(1),
ctc2_read_joystick(2), ctc2_read_joystick(3),
refdvals.ax, refdvals.ay, refdvals.bx, refdvals.by,
biosvals.ax, biosvals.ay, biosvals.bx, biosvals.by);
if (bioskey(1))
if ((bioskey(0) & 0xFF) == 27)
break;
}
exit(0);
}
-------------------------------- snip snip snip --------------------------------
The logic of ctc2_read_joystick() is not obvious so I will explain.
The function only measures one joystick input, and it may have been called
recently, so the input it is about to measure may still be timing out from an
earlier call to ctc2_read_joystick(). The function tests explicitly for this,
and if this is the case, it performs a timeout detection in the first while()
loop, waiting for the monostable output to go low. If the monostable output
does not go low within the timeout period, the function returns -1.
If the monostable output is, or already was, low, then a short delay of about
16 CTC clocks plus overhead is inserted, to give a minimum recovery time for
the monostable circuitry which must discharge or recharge the capacitor fully.
If the monostable is triggered too quickly after it has timed out, the capacitor
might not be fully discharged or recharged, resulting in an unusually short
pulse, because the capacitor doesn't have to charge or discharge so far to reach
the monostable threshold.
Then, CTC channel 2 is programmed with a count of MAXCTCCLOCKS and the joystick
monostables are triggered. This section of code operates with interrupts
locked out. It continually checks the joystick status, and checks whether a
timeout has occurred. A timeout is indicated by the Timer 2 Output signal on
the I/O port at I/O address 61h (62h on the PC and XT). If a timeout occurs,
the function returns -1. If the monostable times out and its status line goes
low within the timeout period, the count in CTC channel 2 is latched, and the
number of elapsed CTC clocks is calculated and returned. The function will
always return within about 2 x MAXCTCCLOCKS CTC clocks (units of 0.838 us) plus
interrupt overhead, unless the CTC is faulty.
See section »» 7.30 for a detailed explanation of the timing method using CTC
channel 2 in this way.
The logic of refd_read_joystick is similar, but it watches for transitions on
the Refresh Detect signal to measure elapsed time. Whenever the monostable
bits in the joystick port value change, the counts[] array is updated with the
Refresh Detect count for the appropriate input pattern. This means that if
more than one monostable times out within the same sample period, the code does
not have to potentially update up to four variables, possibly missing a Refresh
Detect transition. The four returned values are calculated after the timing
critical section has completed. The code also keeps flags for inputs which have
timed out, either in the initial checking phase, or the main timing phase, and
always returns -1 for these inputs.
## 10.4.5 USING THE JOYSTICK PORT FOR GENERAL PURPOSE INPUT
The joystick button inputs can be used as general purpose button or switch
inputs, and can also be driven by logic level signals or by open collector or
open drain logic outputs. If used with a signal direct from a mechanical
contact (e.g. a switch, microswitch, contact, or pushbutton), remember that
the joystick port does not perform hardware debouncing, so this must be
provided by external hardware or provided by software.
Provided that you can tolerate poor accuracy, poor repeatability, poor matching
between channels, and poor temperature stability, you can use the joystick
position inputs as general purpose analogue inputs, but don't fart too close
to them. The inputs should not be voltage-driven, they should be driven from
a variable resistor from a positive supply rail such as the 5V rail (the way
the joystick itself works), or from a positive variable current source. This
gives a roughly linear relationship between resistance and time measured, which
means an inverse (reciprocal) relationship between current and time measured.
A voltage signal can be converted into a variable current signal, and a circuit
to do this is given in Figure 5 in the FIGURES archive. This circuit converts
a positive, ground-referenced voltage into a positive current source that can
be fed into one joystick position input. The relationship between input voltage
and output current is linear. 1V on the input produces an output current of
1mA. The circuit requires a 9-12V supply, which is unfortunately not available
on the joystick port, though you could use a switched capacitor voltage booster
(e.g. the Linear Technology LT1054) or a switching supply (e.g. the Motorola
MC34063 or the National Semiconductor LM2574 series) to produce a higher voltage
rail from the 5V output on the joystick port, but be aware that switching power
supplies can create a lot of electrical noise.
Because the relationship between input voltage and time measured is reciprocal,
a zero input voltage will give an infinite timeout. Obviously this should be
avoided, as it will prevent software from reading the inputs within a reasonable
period of time. This can be prevented by ensuring that the input voltage never
falls below a certain threshold, or it could be prevented by incorporating an
offset in the voltage to current converter. In the very unlikely event that
you are interested in pursuing this, I may be able to help so please drop me
an email message.
## 10.4.6 JOYSTICK LEFT/RIGHT AND UP/DOWN DETECTION
If you simply want to detect whether the joystick is left or right of centre,
or above or below centre, and don't want the overhead of locking interrupts
for several milliseconds at regular intervals, you could use a fast tick
interrupt to poll the joystick port. I would suggest using an interrupt at
about 500 us and working cyclically through three states. On one interrupt,
trigger the joysticks. On the next interrupt, read the monostable states.
On the next interrupt, do nothing. On the next interrupt, you're back to the
first interrupt again, so trigger the monostables again. This will give a
left/right and up/down indication every 1.5 ms, with a fairly low overhead.
## 10.5 THE MOUSE AND MOUSE DRIVER [NOT WRITTEN]
I haven't investigated the mouse or the mouse driver. The format of the serial
data is documented (see {JAM}'s documents for the basic information) but I have
nothing on its use of the timer tick interrupt or the CTC hardware. This
section may (or may not :-) be completed at a later date. Any information is
welcomed. (*)
## 10.6 NETWORKS
I have no experience with networks, so I will quote (paraphrased) from {JAM}'s
documents (see section »» 1.7).
The int 8 overhead is increased when network software is installed, because the
network software uses the interrupt to check whether the network is still
functioning properly. This increase is not really significant. Details are
documented in the Netware book (see the references section). However, the
network card interrupts the processor via the network card's own interrupt,
whenever the processor must process and respond to a data packet. This occurs
even if the computer is not using the network at the time, because the network
still checks regularly that the computer is present. {JAM} continues: "Other
machines were checked with just Pathworks or just Novell and the errors are
similar to this. In fact, for machines using Novell over broadband networks,
delays in the order of 1.5 to 2 milliseconds were not uncommon. The actual
numbers presented here should be taken with a grain of salt; they are going to
differ widely with different networks, loads, CPU speeds, and network cards".
## 10.7 SOUND GENERATION
Though the PWM method of sound generation is widely used, the specific method
of generating it on a PC, described in this section and subsections, was (to
my knowledge) first described Mark Feldman the PC-GPE (PC Games Programmer's
Encyclopedia) guru (see section »» 1.7), and subsequently developed by Peter
Moylan and Tim Channon (see section »» 1.7 and »» 10.7.4). It has probably
been independently developed by others. The documentation and the coding of
the sample program are my own.
The PC's basic beep sound makes the speaker cone move between two positions -
in and out. This is shown by the following 'waveform' which graphs speaker
position (on the vertical axis) against time (horizontal axis).
IN ┌───────────────┐ ┌───────────────┐
CONE │ │ │ │
OUT ───┘ └───────────────┘ └─────
1 2 3 4 5 6 7 8 ms
TIME...
This is digital (on or off) control, and this level of control severely limits
the type and subtlety of the sounds that can be generated. Better sound
requires the speaker to be put in more than two positions. For example, an
8-bit sound card such as a Sound Blaster gives 256 discrete output voltages or
speaker positions, using an analogue signal which can assume any of 256
discrete values, and CDs and good quality sound cards use a 16-bit converter
that gives 65536 discrete values.
A digital control can approximate this to a limited degree using a technique
called Pulse Width Modulation (PWM), where a digital signal made up of pulses
at a high frequency is _averaged_ by the hardware. The width of the pulses is
adjusted ('modulated') and this varies the _average_ voltage of the signal when
it is averaged over a short period of time. If the pulse rate is high enough,
the speaker will not be able to follow the pulses themselves, but will follow
the average value. If the pulse widths, and therefore the average value, are
varied at audio frequency, the average value, and therefore the speaker cone
position, varies at audio frequency, and audible sound is generated.
## 10.7.1 PULSE WIDTH MODULATION (PWM) PRINCIPLE
5V ┌─┐ ┌─┐ ┌─┐
│ │ │ │ │ │ 25% duty cycle
0V ─┘ └─────┘ └─────┘ └──── Average voltage = 1.25V
5V ┌───┐ ┌───┐ ┌───┐
│ │ │ │ │ │ 50% duty cycle
0V ─┘ └───┘ └───┘ └── Average voltage = 2.5V
5V ┌─────┐ ┌─────┐ ┌─────┐
│ │ │ │ │ │ 75% duty cycle
0V ─┘ └─┘ └─┘ └ Average voltage = 3.75V
Simple PWM (shown above) uses a fixed pulse rate, and varies the pulse width.
Notice that the rising edges on the above waveforms are all in sync and regular.
The diagram below shows a PWM pulse stream, with pulse start points marked, and
the corresponding approximate average value, showing the audio content in the
signal. If the pulse rate is high enough, only the audio component is audible.
┌───┐ ┌─────┐ ┌─────┐ ┌───┐ ┌─┐ ┌─┐ ┌───┐ ┌─────┐ ┌─
PWM │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
─┘ └───┘ └─┘ └─┘ └───┘ └─────┘ └─────┘ └───┘ └─┘
^ ^ ^ ^ ^ ^ ^ ^
──────────────── ────────
AVERAGE ──────── ──────── ────────
────────────────
Figure 6 in the FIGURES archive shows this a bit more clearly.
## 10.7.2 PWM AUDIO GENERATION IMPLEMENTATION
PWM audio generation can be done directly by the microprocessor, but this is
unreliable due to memory caching and other factors that may affect the speed
of the processor's operation. The generic method uses CTC channels zero and
two, and gives more consistent operation. Channel zero is used to generate
interrupts at the pulse rate, typically 11kHz or higher, and the int 8 handler
uses channel two to generate the pulses.
## 10.7.3 SAMPLE PROGRAM: DTMF GENERATION USING PWM
The following sample program uses PWM to generate DTMF (dual tone multiple
frequency) tones, also known as touch tones, which are used for signalling
numbers being dialled on a touch tone telephone.
The audio output from the program is very quiet, so I have not been able to
confirm that it will actually dial a telephone, but its main purpose is to
present the techniques and sample code.
This program takes over the timer tick interrupt, operating it at about 18000
interrupts (PWM pulses) per second. It does not chain to the BIOS handler, and
it does not restore the correct DOS time from the RTC on termination. This
program will cause loss of time when run. The time can be corrected by
rebooting the machine.
-------------------------------- snip snip snip --------------------------------
NAME SAMPLE19
; Sample program #19
; Demonstrates DTMF (touch tone) generation using PWM sound techniques
; Part of the PC Timing FAQ / Application notes
; By K. Heidenstrom (kheidens@actrix.gen.nz)
;
; This program assembles into SAMPLE19.COM, a small command-line driven program
; which generates DTMF (dual tone multiple frequency) tones, also known as
; touch tones, using PWM sound techniques through the PC speaker, according to
; the command line parameters.
;
; Save this file to SAMPLE19.ASM and assemble with:
; masm SAMPLE19;
; link SAMPLE19;
; exe2bin SAMPLE19.exe SAMPLE19.com
; or
; tasm SAMPLE19;
; tlink /t SAMPLE19;
;
;
; Note - this program will _not_ run properly under OS/2, Linux, Windows, or
; anything other than plain DOS. If possible, it should be run without EMM386
; or QEMM or any other memory manager, particularly on slower machines such as
; 386SX or slow 386 machines.
PulseDivisor = 66 ; Interrupt rate is 1.1931816666... MHz
; divided by this value
Fifty = PulseDivisor/2 ; Pulse width for roughly 50% duty
; The chosen PulseDivisor of 66 gives an interrupt rate of about 18,079
; interrupts per second.
; The following GW-BASIC program generates the 256-entry sinewave table with
; maximum spans of +/- 16, centre zero, using signed values. The span must
; be chosen so that when two sinewaves are added together and added to the
; 'Fifty' value (which represents half the number of CTC clocks between PWM
; pulses), the range of possible pulse widths is within the tolerance of the
; PWM interrupt rate. In this case, the maximum excursion for two summed
; sinewaves is taken to be +/- 32 (two sinewaves, each at +/- 16). When added
; to the 'Fifty' value (33), the pulse width range is 1 to 65.
; If you change the pulse rate, you must change the span (the 16# in line 20)
; appropriately. I chose a span of roughly (PulseDivisor - 2) / 4.
;
; 10 OPEN "SINE.DMP" FOR OUTPUT AS#1 : A# = 0 : I# = 3.141592653589793#/128#
; 20 FOR P = 0 TO 255 : S# = SIN(A#) : V = INT((S# * 16#) + .5#) : PRINT #1,V
; 30 A# = A# + I# : NEXT : CLOSE #1 : SYSTEM
;
; I used the following GW-BASIC program to generate lists of delta sequences
; for indexing into the 256-entry sinewave table. The A#=... value in line 20
; specifies the sample rate; the last number in the equation is PulseDivisor.
; If you change PulseDivisor, modify this and rerun the program.
; To use the program, input the desired frequency, and it will calculate
; possible delta sequences and prompt you. Initially, just press Enter at
; the prompt, until you have chosen the delta sequence you will use. Then
; break and rerun the program, and at the prompt for the chosen sequence,
; type 'y <enter>'. The program will append to a file '$CALC.DMP' and list
; the delta sequence.
;
; 10 REM $CALC - Calculation for DTMF generator program
; 20 A#=14318180#/12#/66# : INPUT F : PRINT "There are" A#/F "samples/cycle"
; 30 I# = 256 * F / A# : PRINT "Samples are spaced at intervals of" I#
; 40 X# = 0 : D = 1 : R = 0
; 50 R = R + 1 : X# = X# + I# : Z = ABS(X# - INT(X# + .5#)) : IF Z >= D THEN 50
; 60 PRINT R "samples, error is" Z;: D = Z : INPUT Q$ : IF Q$ <> "y" GOTO 50
; 70 OPEN "$CALC.DMP" FOR APPEND AS#1 : PRINT#1, F ":" R "values" : S# = 0
; 80 I = 0 : FOR P = 1 TO R : SD = -I : S# = S# + I# : I = INT(S# + .5#)
; 90 SD = SD + I : PRINT#1, SD;: NEXT : PRINT#1,"" : CLOSE #1 : END
Code SEGMENT
ASSUME cs:Code,ds:Code,es:nothing,ss:nothing
ORG 100h
Begin: jmp Begin2 ; Skip data
SignOnMsg DB "Sample program #19 - DTMF generator demonstrating PWM sound generation",13,10
DB "Part of the PC Timing FAQ / Application notes",13,10
DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10,13,10
DB "Characters 0-9, *, #, and A-D generate DTMF pairs",13,10
DB "Characters a-h generate single tones",13,10
DB "A comma generates a 1/2-second pause",13,10,"$"
ALIGN 2
ParmPointer DW 81h ; Pointer into command tail
OldInt8Ofs DW 0 ; Old int 8 handler offset
OldInt8Seg DW 0 ; Old int 8 handler segment
PWMBufferGet DW 0 ; 'Get' offset for PWMBuffer (volatile)
PWMBufferPut DW 0 ; 'Put' offset for PWMBuffer
PIC0IMR DB 0 ; PIC IMR before we stopped all but IRQ0
ALIGN 2
Tone0Ctrl DW 0,0 ; Delta and sinewave table pointers
Tone1Ctrl DW 0,0 ; Same, for other tone of the pair
CharScanTable: DW "0",Row4,Col2
DW "1",Row1,Col1
DW "2",Row1,Col2
DW "3",Row1,Col3
DW "4",Row2,Col1
DW "5",Row2,Col2
DW "6",Row2,Col3
DW "7",Row3,Col1
DW "8",Row3,Col2
DW "9",Row3,Col3
DW "*",Row4,Col1
DW "#",Row4,Col3
DW "A",Row1,Col4
DW "B",Row2,Col4
DW "C",Row3,Col4
DW "D",Row4,Col4
DW "a",Row1,0
DW "b",Row2,0
DW "c",Row3,0
DW "d",Row4,0
DW "e",Col1,0
DW "f",Col2,0
DW "g",Col3,0
DW "h",Col4,0
PastCharTable = $
Row1 DW 10,10,10,9,10,10,10,10,Row1 ; 697 Hz
Row2 DW 11,11,11,11,11,10,11,11,11,11,Row2 ; 770 Hz
Row3 DW 12,12,12,12,12,12,12,13,12,12,12,12,12,12,12,Row3 ; 852 Hz
Row4 DW 13,14,13,Row4 ; 941 Hz
Col1 DW 17,17,17,17,18,17,17,17,Col1 ; 1209 Hz
Col2 DW 19,19,19,19,19,19,18,19,19,19,19,19,Col2 ; 1336 Hz
Col3 DW 21,21,21,21,21,20,21,21,21,21,21,21,Col3 ; 1477 Hz
Col4 DW 23,23,23,23,24,23,23,23,Col4 ; 1633 Hz
SineTable DB 0,0,1,1,2,2,2,3,3,4,4,4,5,5,5,6,6,6,7,7,8,8,8,9,9,9,10
DB 10,10,10,11,11,11,12,12,12,12,13,13,13,13,14,14,14,14
DB 14,14,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,16,16
DB 16,16,16,16,16,16,16,16,16,16,16,15,15,15,15,15,15,15,14
DB 14,14,14,14,14,13,13,13,13,12,12,12,12,11,11,11,10,10,10
DB 10,9,9,9,8,8,8,7,7,6,6,6,5,5,5,4,4,4,3,3,2,2,2,1,1,0,0,0
DB -1,-1,-2,-2,-2,-3,-3,-4,-4,-4,-5,-5,-5,-6,-6,-6,-7,-7,-8
DB -8,-8,-9,-9,-9,-10,-10,-10,-10,-11,-11,-11,-12,-12,-12
DB -12,-13,-13,-13,-13,-14,-14,-14,-14,-14,-14,-15,-15,-15
DB -15,-15,-15,-15,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16
DB -16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-15,-15,-15
DB -15,-15,-15,-15,-14,-14,-14,-14,-14,-14,-13,-13,-13,-13
DB -12,-12,-12,-12,-11,-11,-11,-10,-10,-10,-10,-9,-9,-9,-8
DB -8,-8,-7,-7,-6,-6,-6,-5,-5,-5,-4,-4,-4,-3,-3,-2,-2,-2,-1
DB -1,0
ALIGN 4 ; No need to do this, really.
PWMBuffer DB 256 DUP(?) ; PWM width-value data buffer (circular)
Begin2: mov dx,OFFSET SignOnMsg ; Point to sign-on message
mov ah,9 ; Function number
int 21h ; Output the message
cld ; Upwards direction
call InitialisePWM ; Initialise and start PWM stuff
DigitLoop: mov bx,ParmPointer ; Get pointer into command tail
inc ParmPointer ; Bump it
mov al,[bx] ; Get character from command tail
cmp al,13 ; End of command tail?
je DigitsDone ; If so
cmp al," " ; Whitespace?
jbe DigitLoop ; If so, skip it
cmp al,"," ; Comma?
jne NotComma ; If not
mov cx,7850 ; If so, pause for half a second
call MakeDelay
jmp SHORT DigitLoop ; Loop
NotComma: mov bx,OFFSET CharScanTable-6 ; Point to before first entry
NextCharScan: add bx,6 ; Point to next
cmp bx,OFFSET PastCharTable ; Scanned whole table?
jae DigitLoop ; If not found, skip it
cmp al,[bx] ; Check for match
jne NextCharScan ; If not, loop
mov ax,[bx+2] ; Get first tone pointer
mov dx,[bx+4] ; Get second tone pointer
mov cx,3140 ; 200 ms duration
call MakeDTMF
mov cx,785 ; 50m ms pause
call MakeDelay
jmp SHORT DigitLoop ; Loop
DigitsDone: call UninstallPWM
mov ax,4C00h
int 21h
int 20h
MakeDTMF PROC near
mov Tone0Ctrl,ax
mov Tone0Ctrl+2,0
mov Tone1Ctrl,dx
mov Tone1Ctrl+2,0
DTMFLoop: mov bx,OFFSET Tone0Ctrl
call GetPulseWidth
mov bx,OFFSET Tone1Ctrl
cmp WORD PTR [bx],0
jz SingleTone
xchg ax,dx
call GetPulseWidth
add al,dl
SingleTone: add al,Fifty
call PutPWM
loop DTMFLoop
ret
MakeDTMF ENDP
MakeDelay PROC near
DelayLoop: mov al,Fifty
call PutPWM
loop DelayLoop
ret
MakeDelay ENDP
GetPulseWidth PROC near ; Call with BX pointing to first or
cld ; second tone control structure
mov si,[bx] ; Get delta table pointer
lodsw ; Get a delta or pointer
test ah,ah ; Was it a pointer?
jz GotDelta ; If not
mov si,ax ; If pointer, reset pointer
lodsw ; Get it, and increment pointer
GotDelta: mov [bx],si ; Return the delta table pointer
mov si,[bx+2] ; Get sine table pointer
add si,ax ; Add delta
and si,0FFh ; Wrap around
mov [bx+2],si ; Restore sine table pointer
mov al,SineTable[si] ; Get sine table entry
ret
GetPulseWidth ENDP
InitialisePWM PROC near
; Get the current int 8 handler address, to be restored later
mov ax,3508h ; Get interrupt vector for int 8
int 21h ; Call DOS
mov OldInt8Ofs,bx ; Store offset
mov OldInt8Seg,es ; Store segment
; Initialise PWM array with 50% duty cycle entries
xor bx,bx ; Zero offset
mov al,Fifty ; 50% duty cycle
FillPWMBuf: mov PWMBuffer[bx],al ; Set entry
inc bl ; Bump offset
jnz FillPWMBuf ; Do all entries
; Wait for all floppy drives to turn off
xor ax,ax ; Zero
mov es,ax ; Address BIOS data area with ES
WaitMotors: test BYTE PTR es:[43Fh],0Fh ; Any floppy drive motors active?
jnz WaitMotors ; If so, wait
; Disable all interrupt sources on the primary (or only) PIC. Keep the
; original IMR contents, to be restored later.
cli
in al,21h ; Read primary PIC IMR
jmp SHORT $+2 ; Short delay
mov PIC0IMR,al ; Store it for later
mov al,0FFh ; Mask off _everything_
out 21h,al
; Set up Port B and CTC channel 2
in al,61h ; Read Port B
jmp SHORT $+2 ; Short delay
and al,11111101b ; Speaker enable OFF
or al,00000001b ; Timer 2 gate ON
out 61h,al ; Write it back
jmp SHORT $+2 ; Short delay
mov al,10010000b ; Channel 2, lobyte access, mode 0
out 43h,al ; Set mode of channel 2 (no values yet)
sti ; Allow interrupts
; Now grab int 8, the timer tick interrupt. DOS should leave the PIC IMR alone.
mov dx,OFFSET NewInt8 ; Offset of new int 8 routine
mov ax,2508h ; Set interrupt vector for int 8
int 21h ; Call DOS to do it
; Reprogram CTC channel 0 with the new interrupt rate
cli ; Lock out interrupts again
mov al,00110110b ; Channel 0, lobyte/hibyte, mode 3
out 43h,al ; Write mode/command register
jmp SHORT $+2 ; Short delay
mov al,LOW PulseDivisor ; Lobyte of new divisor
out 40h,al ; Send it
jmp SHORT $+2 ; Short delay
mov al,HIGH PulseDivisor ; Hibyte of new divisor
out 40h,al ; Send it
jmp SHORT $+2 ; Short delay
; Enable the speaker
in al,61h
jmp SHORT $+2 ; Short delay
or al,00000011b ; Timer 2 gate and speaker enable ON.
out 61h,al
jmp SHORT $+2 ; Short delay
; Enable int 8 (IRQ0) in the PIC
mov al,11111110b ; All masked except IRQ0
out 21h,al ; Set primary PIC IMR
jmp SHORT $+2 ; Short delay
; Start interrupts and return to caller
sti ; Tag! You're it :-)
nop
mov al,BYTE PTR PWMBufferGet ; Get 'get' pointer
dec ax ; Back up one
mov BYTE PTR PWMBufferPut,al ; Output one bufferful
ret
InitialisePWM ENDP
UninstallPWM PROC near
; Disable the speaker
pushf ; Preserve interrupt flag
cli ; Lock out interrupts around this
in al,61h ; Read Port B
jmp SHORT $+2 ; Short delay
and al,11111100b ; Disable Timer 2 and speaker
out 61h,al ; Write it back
jmp SHORT $+2 ; Short delay
; Disable all interrupt sources in the primary PIC
mov al,0FFh ; Mask all IRQ0-7
out 21h,al ; Set PIC0 IMR
; Restore normal operation of CTC channel 0
mov al,00110110b ; Channel 0, lobyte/hibyte, mode 3
out 43h,al ; Write mode/command word
jmp SHORT $+2 ; Short delay
xor al,al ; Zero
out 40h,al ; Write loword of reload value
jmp SHORT $+2 ; Short delay
out 40h,al ; Write hiword of reload value
popf ; Interrupts are safe (PIC is blocked)
; Restore original int 8 handler address
push ds ; Will need to destroy DS for this
mov dx,OldInt8Ofs ; Get offset
mov ds,OldInt8Seg ; Get segment
ASSUME ds:nothing ; DS no longer points to this segment
mov ax,2508h ; Set int 8 vector
int 21h ; Call DOS
pop ds ; Restore DS
; Restore original IMR
mov al,PIC0IMR ; Get old IMR contents
out 21h,al ; Restore IMR
ret
UninstallPWM ENDP
; The following function stuffs a pulse width value into the circular buffer,
; first waiting for the interrupt routine's outgoing data pointer to catch up,
; if necessary. This prevents the foreground code from generating data more
; quickly than the interrupt routine is taking it, and maintains synchronisation
; between the two processes, unless the foreground code generates the data too
; slowly.
PutPWM PROC near ; Put width in AL into buffer
mov bx,PWMBufferPut ; Get the 'put' offset
mov PWMBuffer[bx],al ; Store the width value in buffer
inc bl ; Bump 'put' pointer
mov PWMBufferPut,bx ; Store it back
WaitBufFull: cmp bl,BYTE PTR PWMBufferGet ; If buffer is full...
je WaitBufFull ; ... wait until there's a gap
ret
PutPWM ENDP
ASSUME ds:nothing
; This program uses CTC channel 0 as a timebase, generating int 8 at regular
; intervals, and CTC channel 2 producing variable width pulses. The interrupt
; routine programs an 8-bit count value into channel 2 on every invocation, and
; channel 2 produces a pulse of the corresponding length on the speaker output
; signal. Because the interrupt rate is constant and the pulse width varies,
; pulse width modulation (PWM) sound is generated.
; This is the int 8 handler. It gets data from PWMBuffer, using PWMBufferGet
; as an offset into PWMBuffer indicating where it's currently up to. It bumps
; this variable on each timer interrupt. The bump increments the lobyte only,
; so that the offset wraps around from 255 back to 0 again (the buffer is 256
; bytes in size). This code does not check to see whether PWMBufferGet has
; been bumped past PWMBufferPut. The foreground code must be fast enough to
; keep the buffer full - if not, the int 8 processing will repeat-play the
; buffer.
; Each entry in the buffer is one byte, and corresponds to the pulse width of
; one pulse. The data in this buffer is generated by the foreground code.
; The following code could be optimised somewhat - by page-aligning the
; PWM buffer, the MOV AL,PWMBuffer[BX] could be replaced with a direct load
; using a self-modified pointer, also removing the need to preserve BX.
NewInt8 PROC far
push bx ; Preserve
push ax ; Preserve
mov bx,PWMBufferGet ; Get pointer to data coming from buffer
mov al,PWMBuffer[bx] ; Get one pulse-width byte from buffer
out 42h,al ; Tell CTC channel 2 to make a pulse
inc BYTE PTR PWMBufferGet ; Bump pointer (256-byte buffer)
mov al,20h ; EOI command
out 20h,al ; Send to primary PIC
pop ax ; Restore
pop bx ; Restore
iret ; Return from interrupt
NewInt8 ENDP
Code ENDS
END Begin
-------------------------------- snip snip snip --------------------------------
## 10.7.3.1 SAMPLE PROGRAM EXPLANATION
Channel 0 is operated in mode 2 or 3, and generates interrupts (int 8) at
regular intervals. Each int 8 will trigger the start of one pulse. The int
8 handler, NewInt8, will output a pulse-width value to the CTC channel 2 data
register, and CTC channel 2 will produce a pulse of the corresponding length.
Channel 2 is operated in mode 0, known as 'interrupt on terminal count' mode
(see section »» 7.8.2). When CTC channel 2 has been initialised for this mode,
and the Timer 2 Gate output in the Port B register (see section »» 7.5) is set
to enable clocking of CTC channel 2, writing a count value to the channel 2
reload register will cause the CTC channel 2 output to go low for a period of
time determined by the value written to the channel 2 register. By controlling
the values written to the channel 2 register, the pulse width can be varied.
The pulse width will be the CTC clock period (0.838 us) multiplied by the value
written to the channel 2 register. To improve efficiency, because pulses are
typically much less than 256 CTC clocks wide, CTC channel 2 is configured in
lobyte-only access mode. Only one I/O access, to write a byte of data to CTC
channel 2, is required to trigger a pulse on CTC channel 2. If the Speaker
Data bit in Port B is set, the pulse will be sent to the PC's speaker.
The above description covers the PWM output code, which consists of an interrupt
handler, triggered at regular intervals via int 8 from CTC channel 0. The
handler writes an 8-bit pulse-width value to the CTC channel 2 data register.
The data that it writes is taken from a circular buffer. The interrupt handler
maintains a pointer, the 'get' pointer, called PWMBufferGet, which lets it keep
track of where it is up to in the buffer. On every interrupt, it loads the BX
register from the 'get' pointer, reads a pulse-width value from the circular
buffer at the appropriate position, and 'bumps' the 'get' pointer. The term
'bump' means to increment, but in this case, also wrap around from the end of
the buffer to the start, as the buffer is circular.
The actual data in the buffer is generated by the foreground code, and inserted
into the buffer by the PutPWM function. This function maintains synchronisation
between the foreground code and the interrupt handler, by checking that it will
not overfill the buffer, before putting a byte into the buffer. This slows down
the mainline code. As long as the mainline runs quickly enough, synchronisation
between the mainline and the interrupt routine is maintained.
The initialisation steps are fairly involved. All initialisation is done by
the InitialisePWM function. First, the current int 8 handler address is stored
so it can be restored later. Then the circular PWM buffer is filled with the
'Fifty' value, so that when the interrupt is started later, before the mainline
has filled the buffer, it will play silence instead of garbage. The code then
waits for all floppy drives to turn off. Because the replacement int 8 handler
does not chain to the original handler, any actions normally done by the int 8
handler, such as updating the BIOS timer tick count variable and turning off
floppy drives after two seconds of inactivity, will not be performed during the
execution of this program, so we must wait for the disk drives to turn off
before replacing the int 8 handler, otherwise they will remain on during the
program's execution.
The initialisation code then disables all interrupt sources on the primary
interrupt controller. IRQ0 (int 8), the timer tick interrupt, will be enabled
shortly. The code then initialises Port B, initially with Speaker Enable off,
and programs the operating mode (mode 0, interrupt on terminal count) and the
access mod (lobyte-only) in CTC channel 2. It then redirects int 8 to its own
int 8 handler, reprograms CTC channel 0 with the new interrupt rate (18,079
interrupts per second), enables the Speaker Enable, and enables IRQ0 in the
interrupt controller. Other interrupt sources are not enabled in the interrupt
mask register of the primary PIC. This prevents interrupts due to a keypress
from disturbing the sound generated. The system will not respond to keypresses
while the program is running. It then enables interrupts, resets the 'put'
pointer for the PWM buffer, and returns.
Once this initialisation has been done, the interrupt routine will run quite
happily in the background, outputting pulse widths from the circular buffer of
pulse-width values. It will loop repeatedly through the buffer. Foreground
code is required to set up the data in the buffer, and keep track of the 'get'
pointer used by the interrupt routine, so it can control the flow of data into
the circular buffer.
The actual DTMF waveform generation is done via a 256-entry sinewave table with
a span of +/- 16. The table contains one cycle of sinewave, and is indexed via
a delta sequence table. Each of the eight possible frequencies has its own
delta sequence table. The delta sequence table tells the program how many
entries in the sinewave table to skip between PWM pulses (samples). For a high
frequency, the deltas are large, so the program steps through the 256 entries
of the sinewave table fairly quickly, and for lower frequencies, the delta is
smaller. A table of deltas is required, to give the effect of a non-integral
delta value so that reasonable frequency accuracy can be achieved.
Running int 8 at these high rates causes a significant load on the machine,
especially with slower machines. Using EMM386 adds interrupt overhead, and on
slower machines, programs using this technique may not run properly with EMM386
installed. I have done limited testing with the sample program, and found that
it works properly on a 10MHz 286, but I can't guarantee its performance on, say,
a 386SX-16 running EMM386, or on an XT.
## 10.7.3.2 OTHER METHODS OF SOUND GENERATION
The same fast int 8 handler can be modified to output an 8-bit unsigned sample
value to a parallel port, which is connected to a DAC (digital to analogue
converter). This gives much better sound quality than the PWM technique.
The digital to analogue converter can be a chip, such as the Ferranti ZN429
or various devices from other manufacturers such as Analog Devices / PMI,
Maxim, Burr-Brown, etc, or the el cheapo R-2R ladder DAC made from a chain of
resistors. Commercial parallel port DAC units are available - the Speech
Thing device is just a DAC on the parallel port.
Sound cards have an 8-bit or 16-bit DAC, but are usually operated in DMA mode,
where the sound card periodically requests an 8-bit or 16-bit data transfer
from a buffer area in system memory and sends the value to the DAC. The DMA
method gives much lower overhead, because the processor does not get involved
in the transfer, and also removes the problem of sample timing jitter.
## 10.7.4 PETER MOYLAN'S MUSIC PACKAGE
Peter Moylan's music package was written by Peter Moylan (see section »» 1.7)
and Tim Channon. It uses CTC channel 0 with a divisor of 64, and CTC channel
2 in interrupt on terminal count mode (mode 0). It does not chain to the
original int 8 handler, and does not fix up the DOS time on termination. This
package produces 3-part polyphonic (i.e. three simultaneous pitches) music and
supports several timbral qualities. It is noticeably out of tune, particularly
at higher pitches, but this is due to limitations in the waveform generation
algorithm, not the hardware technique used. Seven demonstration programs and
source code in Modula-2 and assembler are included in the package, which is
available on the Internet as: ftp://ee.newcastle.edu.au/pub/PMOS/music302.zip.
The version number (3.02) may have changed.
## 10.8 RELATED SOFTWARE PACKAGES
Here are my comments on some timing-related software packages available on the
Internet. Many of these packages are several years old, so contact details may
be well out of date. I have not checked any of the contact details.
## 10.8.1 THE ATIM PACKAGE
ftp://oak.oakland.edu/SimTel/msdos/at/atim.zip
Date: 19881125 Size: 4783
This package contains a small program called ATIM which will run another
program and time its execution, using the RTC periodic interrupt for timing
(approximately one millisecond resolution). The program is written in
assembler, and commented source and brief documentation is included. The
package was written by Howard Vigorita, NYACC (whatever that means :-),
December 27, 1986. I presume it is public domain, though he doesn't say so.
The program seems to work quite nicely. I'm not sure about the algorithm he
uses to convert 1/1024ths to 1/1000ths of seconds, though. The only problem
I noticed was that "COMMAND.COM" was hardcoded as the command interpreter name
if the program is assembled to use the DOS EXEC function instead of the back
door execute function - after all, this program is nearly nine years old!
## 10.8.2 THE MSCHRT AND TCHRT PACKAGES
ftp://oak.oakland.edu/SimTel/msdos/c/mschrt3.zip
Date: 19910604 Size: 53708
ftp://oak.oakland.edu/SimTel/msdos/turbo_c/tchrt3.zip
Date: 19910605 Size: 53436
MSCHRT and TCHRT version 3 are Microsoft C and Turbo C compatible versions of
a "high resolution timer toolbox" distributed as a library, from Ryle Design,
P.O. Box 22, Mt. Pleasant, Michigan 48804, (517) 773-0587, CI$ 73047,1765.
They also have an equivalent package for Turbo Pascal, called TPHRT. The
package is shareware, $20 per copy. This company also sells a fully-functional
timing toolbox called PCHRT, version 4, which also supports running the timer
tick interrupt at a user-specified rate, and can be ordered from Ryle Design
(order form included with MSCHRT V3 and TCHRT V3) for $49.95. This, and
registered versions of MSCHRT, TCHRT, and presumably TPHRT, include library
source and support. I have not checked that they are still contactable.
The is clearly a very professionally designed package, which includes thorough
documentation and has obviously been designed to make the user's task as easy
and successful as possible. It provides 42 functions including the ability to
produce formatted reports! It includes a self-calibration function, presumably
to take into account the different amount of time required to read a timestamp
on different machines.
The timing functions can operate with interrupts enabled, or disabled (in this
case, periods longer than 54.925 ms will not be measured correctly). It
presumably sets CTC channel zero to mode 2, though the manual doesn't describe
this correctly.
Suggested applications are: timer or profiler to determine code performance,
benchmarking programs, precise delays for hardware or process control, subject
testing (e.g. reaction timing, race timing and scoring systems).
The package also supports profiling and reporting on BIOS function interrupts
(e.g. int 10h video, int 13h disk) - the vector is hooked and logging logic is
installed, then complete information can be generated for that interrupt.
Functions specifically to delay a specified length of time are also available.
The package includes the library file, explanatory material, function reference,
and five demo programs.
There is no date in the manual, but the newest file in the archive is dated
19900723. I did not test this package - it's probably safe to assume that it
works well.
## 10.8.3 THE TCTIMER PACKAGE
ftp://oak.oakland.edu/SimTel/msdos/turbo_c/tctimer.zip
Date: 19891029 Size: 15609
This is a public domain absolute timestamping package for Turbo C. It contains
functions to enable mode 2 on CTC channel zero, to restore normal operation in
mode 3 at exit, to read an absolute timestamp, and to calculate elapsed time in
units of one microsecond using floating point arithmetic. The timestamp value
is comprised of the count in progress and the bottom 16 bits of the BIOS timer
tick variable, returned as a long (dword), therefore periods longer than one
hour cannot be measured (this is mentioned in the documentation file).
The documentation file says it was "written by Richard S. Sadowsky, 8/10/88,
Version 1.0, released to the public domain, based on TPTIME.ARC which was
written by Brian Foley and Kim Kokkonen of TurboPower Software and released
to the public domain". Source code is included.
This package appears to have the following problems.
1. Registers SI and DI are not preserved by the readtimer() function,
usually causing the calling function to crash if it uses register
variables.
2. The readtimer() function has many unnecessary I/O accesses and is
fairly slow as a result.
3. Timing will be incorrect if the timed period spans a change of day,
because just before midnight the loword of the BIOS tick count
counts to 0AF hex then resets. This is not handled by this package.
I tested the code briefly, after fixing the SI and DI problem, and it appeared
to work correctly (apart from the midnight problem).
## 10.8.4 THE MILLISEC PACKAGE
ftp://oak.oakland.edu/SimTel/msdos/c/millisec.zip
Date: 19911204 Size: 37734
This package was released by Fred C. Smith (uunet!samsung!wizvax!fcshome!fredex)
and is a modified version of a release by Dean Pentcheff (dean@violet.berkeley
.edu) which is a modified version of the TCTIMER package (see previous section).
Source is included.
At some stage in the evolution of this package, the resolution seems to have
been reduced to one millisecond. Dean Pentcheff's package (the 'missing link'
:-) apparently returned elapsed time as a floating point number in units of one
second, with three decimal places. This package returns elapsed time in units
of one millisecond, to avoid floating point calculations. Also the CTC clock
has been approximated to 1193000 Hz, resulting in a proportional error of
152.254 ppm (0.0152254%; 13.155 seconds per day).
These routines use CTC channel zero in mode 2, as per the TCTIMER package, and
the timer-reading function is identical to TCTIMER's one. The problems that I
noted for TCTIMER still apply to this package.
## 10.8.5 THE MSEC_12 PACKAGE
ftp://oak.oakland.edu/SimTel/msdos/c/msec_12.zip
Date: 19920319 Size: 8484
This package was released by David Kirschbaum (kirsch@usasoc.soc.mil) and is
a further modification of the MILLISEC package (see the previous section).
David has moved the inline assembly stuff into a separate file, and fixed the
problem with destroying SI and DI, though the rest of the read-timer function
is the same as that of the TCTIMER package, so the remaining two problems are
still present.
The package uses one millisecond resolution, and approximates the CTC clock to
1193000 Hz, resulting in a proportional error of 152.254 ppm (0.0152254%;
13.155 seconds per day).
Source and makefiles for TCC, BCC, and QC are included.
## 10.8.6 THE ERTIMER PACKAGE
ftp://x2ftp.oulu.fi/pub/msdos/programming/docs/ertimer.zip
Date: 19950506 Size: 9092
This ZIP file contains a message, a header file, and a C source file for an
includable timing module that provides a user-selectable number of independent
timers, each with 0.8381 us resolution, implemented via CTC channel 0 operating
in mode 2 and using the loword of the BIOS timer tick count variable. Written
by Ethan Rohrer, comments dated 19941204. Nicely written and fairly well
commented, but cannot measure times longer than about an hour, and does not
handle the problem of the CTC count synchronisation with the BIOS tick count,
nor the midnight wraparound where the loword of the tick count counts to 0AF
hex then wraps back to zero. Also does not lock out interrupts around hardware
access sequences. Not reliable.
## 10.8.7 THE FASTCLOK PACKAGE
ftp://x2ftp.oulu.fi/pub/msdos/programming/docs/fastclok.zip
Date: 19950506 Size: 2588
This package consists of a C source file and a header file. The package runs
the timer tick interrupt at 64 times its normal speed, using its own interrupt
handler which chains to the BIOS handler correctly. Does not lock interrupts
properly when installing and uninstalling. It installs an atexit() function
to uninstall the fast timer and restore normal operation. The author does not
identify him/herself. A comment in the source file says:
"The gettimeofday() routine acts like the Unix version, with the
exception that time zone does not matter. The time will be returned
in timeval structures that match thier Unix counterparts".
The program doesn't seem to include a gettimeofday() function, though. :-\
## 10.9 BENCHMARKING CONSIDERATIONS
When using absolute timestamping to benchmark a section of code, remember that
because interrupts are enabled during execution of the code being timed, they
will contribute to the time measured.
During otherwise idle time, the timer tick interrupt will be active (every
54.9254 ms), the keyboard keystroke interrupt will occur every time a key is
pressed or released, or repeatedly while the key is held down, and if a mouse
driver is installed and enabled, the mouse's interrupt will occur several
times every time the mouse is moved or the buttons change state.
If the code being timed takes a short time, e.g. less than 100 milliseconds,
the effect of the timer tick interrupt may be detectable. If the period is
shorter than 54.9 ms, it can be measured with interrupts locked out, because
interrupts are only required to ensure that the BIOS tick count variable is
updated correctly on every cycle of CTC channel zero.
The other factors can be avoided by not touching the keyboard or mouse during
the test.
Other factors have an effect on benchmarks, such as the processor cache state
and, for file processing programs, the disk cache state. The latter problem
can be avoided by disabling the disk cache, or ensuring that the input file
is already in the cache (providing that the cache is big enough to hold it) by
entering 'copy /b filename nul:' to force the entire file to be read from disk.
Finally, adding the code which reads the timer adds to the execution time.
For example, if you call a function to read an absolute timestamp twice in
succession, the times read will differ by the amount of time taken to read the
timestamp. For example, the assembly language get-timestamp function given
in the sample program in section »» 9.2 takes between 7 and 9 CTC clocks (about
6.5 us) to execute on my 486DX2-66.
I have no experience or information on ways to determine processor clock speed.
If anyone can help, please let me know. (*)
## 10.10 GRANULARITY AND UNCERTAINTY
This may seem obvious, but the accuracy of any time measurement is limited by
the granularity of the timing source, and its uncertainty. Granularity, or
resolution, refers to the fineness of the unit in which the time or duration
can be measured. For example, using 54.9254 ms timer ticks to measure the
time taken by a short section of code is going to be of limited use. On most
of the test runs, no time will appear to have elapsed, but occasionally, one
tick, or 54.9254 ms, will appear to have elapsed. The resolution is not high
enough, and a different approach is required - for example, running the section
of code repeatedly in a loop, and measuring the total time taken.
If 1000 iterations of the code are timed using the timer tick, by sampling the
BIOS Tick Count variable, running the code 1000 times, then re-reading the BIOS
Tick Count and using the difference in tick counts to calculate the amount of
time elapsed, we might find that five ticks, or about 275 ms, have elapsed, but
how accurate is this figure?
Code execution ───────────────███████████████████████████████████───────
Timer ticks ────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───
1 2 3 4 5 6 7 8
(You will need a monospaced display to see the above diagram properly).
In the above example, when the code started, the tick count was 2. When it
finished, the tick count was 7. The execution time was 5 ticks.
Code execution ────────────█████████████████████████████████████████────
Timer ticks ────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───
1 2 3 4 5 6 7 8
Above, when the code started, the tick count had just changed to 2, and when it
finished, the tick count was 7, just about to change to 8. The measured time
was 5 ticks, as before, but the actual execution time was nearly 6 ticks.
Code execution ──────────────────█████████████████████████████──────────
Timer ticks ────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───
1 2 3 4 5 6 7 8
Above is the opposite case. The measured time is again from 2 to 7, 5 ticks,
but the execution time was actually only slightly longer than 4 ticks.
These examples demonstrate uncertainty of up to one tick at both the start and
the end of the sampling time. The uncertainty at the start of the sample is
due to the granularity, or resolution, of the timing source, and the fact that
it is free-running or asynchronous (not synchronised) to the event being timed.
The uncertainty at the end of the sampling time is the unavoidable effect of
the resolution of the timing source. The total uncertainty of the sample is
two ticks.
If we wait for the tick count to change, then start the code, we can eliminate
(or greatly reduce) the uncertainty at the start of the sampling time.
The worst cases would then be:
Code execution ────────────███████████████████████████████████──────────
Timer ticks ────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───
1 2 3 4 5 6 7 8
and
Code execution ────────────█████████████████████████████████████████────
Timer ticks ────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───
1 2 3 4 5 6 7 8
Sometimes it is possible to synchronise the time reference and the event to be
timed, either by delaying the start of the event (as in the above example) or
by starting the time reference from a known part of its cycle when the start of
the event is detected.
Sometimes it is not possible to do this. For example, the Refresh Detect signal
described in section »» 7.37 has a period of about 15 us, but cannot safely be
stopped and restarted at a particular point so that it is synchronised to the
start of some event. When using such a time base, you must either synchronise
the event to the time base (as in the third method of reading the joystick
position in section »» 10.4.4) or live with the fact that there is a 30 us
uncertainty in any event that is timed using this method.
Also see the sample program section »» 4.7 (timeouts implemented using the
timer tick) where the uncertainty is actually at the _start_ of the timing
period, not at the end.
## 10.11 CONVERTING BETWEEN MICROSECONDS AND CTC CLOCKS
Conversion between microseconds and CTC clocks requires fairly accurate
arithmetic, namely multiplication by 1.193181666... or 0.838095...
This can be done using floating point, however this is slow on machines without
a math coprocessor, and is inefficient, and does not necessarily give very good
accuracy, even if you are not using a Pentium :-) And as floating point is not
usually required in the remainder of the program, it seems silly to require it
for this purpose only.
For comparatively painless implementation on all x86 processors under DOS, the
method described here uses a function that multiplies two long values (32 bits
each) together, giving a 64-bit result, and returns the top 32 bits of the
result as a 32-bit long. If bit 31 of the 64-bit result is set, then the
return value is rounded up.
Using longs to represent microseconds or CTC clocks limits the maximum period
that can be expressed to about 59 minutes and 59.592 seconds (0xFFFFFFFFL CTC
clocks), i.e. slightly less than one hour.
The function definition follows. I have used Borland's register pseudovariables
(_AX and _DX), so this must be changed for other compilers.
-------------------------------- snip snip snip --------------------------------
unsigned long mul64shift32(unsigned long value, unsigned long mult) {
asm {
push si
mov ax,WORD PTR value
mul WORD PTR mult
mov si,dx
mov ax,WORD PTR value+2
mul WORD PTR mult+2
mov bx,ax
mov cx,dx
mov ax,WORD PTR value
mul WORD PTR mult+2
add si,ax
adc bx,dx
adc cx,0
mov ax,WORD PTR value+2
mul WORD PTR mult
add si,ax
adc bx,dx
adc cx,0
shl si,1
adc bx,0
adc cx,0
mov ax,bx
mov dx,cx
pop si
}
return (_DX << 16) + _AX; /* Should optimise out to nothing */
}
-------------------------------- snip snip snip --------------------------------
The arithmetic expression for this function is:
return_value = int ((value * mult / (2^32)) + 0.5)
Note there is no way for overflow to occur in this function, because even with
value and mult of 0xFFFFFFFFL, the 64-bit result is only 0xFFFFFFFE00000001.
This function can be used in the conversion of microseconds to CTC clocks and
vice versa, by the appropriate choice of the 'mult' value. The 'mult' value
is defined as the desired multiplication factor (e.g. 0.838...) multiplied by
2^32.
For conversion from CTC clocks to microseconds (multiplication by 0.838095...),
the 'mult' value is 3599592096L (0xD68D6AA0L). For conversion from microseconds
to CTC clocks (multiplication by 1.193181666...), 'mult' would be 5124676237,
which is too large to express as a long (because the factor of 1.193181666...
is greater than 1), so this conversion is done by multiplying by the fractional
part of the conversion factor, 0.193181666..., then adding the original value.
The fractional part of the conversion factor equates to a 'mult' value of
829708941L (0x31745A8DL).
Here are the two conversion functions, which use mul64shift32() internally.
-------------------------------- snip snip snip --------------------------------
unsigned long clocks_to_usec(unsigned long clocks) {
return mul64shift32(clocks, 3599592096L);
}
unsigned long usec_to_clocks(unsigned long usecs) {
if (usecs > 3599592094L)
return 0xFFFFFFFFL;
return usecs + mul64shift32(usecs, 829708941L);
}
-------------------------------- snip snip snip --------------------------------
Note the check in usec_to_clocks(). The maximum number of microseconds that
can be represented by a 32-bit number of CTC clocks is 3599592095, which
equates to 0xFFFFFFFFL CTC clocks. This represents a time of about 59 minutes
and 59.592 seconds, just under one hour.
Because of the unrelated units of the two quantities, conversion between clocks
and microseconds using integer values inevitably introduces rounding errors,
so conversions should not be done cumulatively. For example, if you are
summing several durations, your measurements should be kept in clocks and
converted to microseconds after the summation.
Other than integer rounding error, the above functions contribute a proportional
error of less than 0.00000001% (0.0001 ppm, 9 us per day, about five orders of
magnitude better than typical crystal accuracy :-).
## 10.12 MAINTAINING A MILLISECOND OR MICROSECOND COUNT
The sample program in section »» 4.7 uses the BIOS Tick Count variable as a
time indication. This variable is in units of one tick, i.e. 54.9254 ms.
There may be cases where you want to maintain a time value which is in units
of some more sensible value, for example, milliseconds, or maybe microseconds.
Converting between absolute tick count and absolute milliseconds is messy, but
it is easy to maintain a variable, in units of one millisecond or microsecond,
which is updated cumulatively using the timer tick. For example, you could
define a 32-bit variable that will contain the number of milliseconds since
the program started, and call this the milliseconds variable.
When and where the milliseconds variable is updated depends on your program
design. The variable needs to be updated every time a timer tick occurs.
You can achieve this by hooking int 1C hex (see section »» 6.35 and »» 6.36)
or by hooking int 8 (see section »» 6.33), or if there is a convenient 'idle'
point in your program where it can read the BIOS tick count variable, the
update can be done there, by checking whether the tick count has changed from
the previous tick count, and if so, updating the millisecond variable and
updating the previous tick count, but with this last method, the logic is a
bit untidy because the update must behave correctly if more than one tick has
elapsed since the update routine was last called.
Updating the milliseconds variable involves adding the number of milliseconds
that have elapsed, into the variable. If CTC channel zero is running with its
normal divisor of 65536, every timer tick interrupt represents 54.9254 ms of
elapsed time. But since the milliseconds variable is a 32-bit integer (no
fractional part), you can't add 54.9254 to it. You have to keep another
variable that keeps track of the remainder. On most interrupts, you will add
55 to the milliseconds variable, but on some interrupts, you will add only 54.
This can be done using a scheduling variable to control whether the 'add' value
will be 54, or 55.
On every interrupt, we will add either 54 or 55 to the milliseconds variable.
But the elapsed time is 54.9254 ms. A remainder variable keeps track of the
fractional part of the real time, and allows us to decide whether to add 54 or
to add 55.
The fractional part of the tick period (in milliseconds) is 0.9254, or more
accurately, 0.9254164984656, which is roughly 12/13. 65536 multiplied by this
value is about 60648. If we add 60648 to a 16-bit count which represents a
number of 1/65536ths-of-a-millisecond, every time the addition carries (which
will be about 12 out of every 13 times), another millisecond has accumulated,
so we would add 55 to the millisecond variable. If the remainder variable did
not carry after adding the 60648, we would add 54 to the milliseconds instead.
Over a reasonable period of time, and (most importantly) over a long period of
time, the milliseconds variable will be accurate. The error contributed by this
technique (due to approximating 65536 x 0.9254... to 60648) is only 0.02657 ppm,
or less than one second per year. The error contributed by crystal inaccuracy
will be about three orders of magnitude higher.
The code to do the update comes out very nicely in assembler:
add Remainder,60648 ; Add 65536 x 0.9254
adc MillisecL,54 ; Add 54 or 55 to loword
adc MillisecH,0 ; Carry into hiword
The three variables Remainder, MillisecL, and MillisecH, are all 16-bit.
MillisecL and MillisecH are loword and hiword of the milliseconds variable.
For a microsecond counter, the same technique applies, but instead of adding
54 or 55 on each tick, you are adding 54925, and the remainder is 65536 x
0.4164984656, or 27295.
add Remainder,27295 ; Add 65536 x 0.4164984656
adc MicrosecL,54925 ; Add 54925 or 54926 to loword
adc MicrosecH,0 ; Carry into hiword
These techniques don't magically give you millisecond or microsecond timing
resolution from a 54.9254 ms clock tick, of course. The resolution is still
only 54.9254 ms. But they do provide a way to get a time value with a sensible
unit.
The same technique can be used when the timer tick is operated at a faster rate
(see section »» 8 and subsections), though the constants change. For example,
to get an actual timing resolution of about 500 us, you could use a channel 0
divisor of 596, giving an interrupt rate of one tick every 499.504825334 us.
Using a microsecond variable, the update would add 499 plus the carry from
adding 33084 to the remainder variable, and 499 plus carry to the microseconds
variable:
add Remainder,33084 ; Add 65536 x 0.504825334
adc MicrosecL,499 ; Add 499 or 500 to loword
adc MicrosecH,0 ; Carry into hiword
With these values, cumulative error due to approximation of 65536 x 0.5048...
to 33084, is 0.00712 ppm.
Choosing to use a millisecond timing variable may make your program easier to
port to (or from) an environment where the system time is kept in units of one
millisecond. For example, OS/2's system time is kept in units of 1ms, though
it does not have a 1ms resolution - is actually only updated every 31.25 ms.
## 10.12.1 SAMPLE PROGRAM: MILLISECOND COUNT USING INT 1CH
The following program uses int 1Ch with the critical error handling module from
section »» 5.8, and demonstrates maintaining a milliseconds count. The timing
resolution of the program is only 54.9254 ms, as it does not modify the timer
tick rate, but the time is reported in units of one millisecond, rather than
units of 54.9254 ms.
Int 1Ch should not be used in TSRs - see section »» 6.35 for details.
Every time the user presses a key, the current millisecond count is displayed.
Pressing the Escape key terminates the program.
-------------------------------- snip snip snip --------------------------------
/*
Sample program #20
Demonstrates a milliseconds count using int 1Ch
Part of the PC Timing FAQ / Application notes
By K. Heidenstrom (kheidens@actrix.gen.nz)
Save and assemble the critical error module CRIT_ERR
Save this sample code to SAMPLE20.C
Compile this module with:
bcc -c -I<inc_path> -ms sample20.c
Link the modules with:
tlink /c /x <c0_path>\c0s.obj sample20.obj crit_err.obj,
sample20, nul, <lib_path>\cs
Where inc_path is the path to your C header files, c0_path is the path to your
startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB.
*/
#include <dos.h> /* Needed for enable(), disable(), MK_FP() */
#include <io.h> /* Needed for _open() and _write() */
#include <stdio.h> /* Needed for printf() */
#include <stdlib.h> /* Needed for exit() */
#define FALSE 0
#define TRUE 1
#define STDERR 2 /* DOS handle for standard error */
void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */
unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */
typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */
intfuncp old_int_1Ch = (intfuncp)0xFFFFFFFFL;
static unsigned int remainder;
static volatile unsigned long milliseconds;
void abort_cleanup(int dos_is_safe) {
if (dos_is_safe) {
if (old_int_1Ch != (intfuncp)0xFFFFFFFFL) {
setvect(0x1C, old_int_1Ch);
old_int_1Ch = (void far *)0xFFFFFFFFL;
}
}
else {
disable(); /* Probably superfluous */
if (old_int_1Ch != (intfuncp)0xFFFFFFFFL) {
*((intfuncp far *)MK_FP(0, 0x1C << 2)) = old_int_1Ch;
old_int_1Ch = (void far *)0xFFFFFFFFL;
}
}
return;
}
void interrupt ctrl_c_handler(void) {
static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n";
if (is_at_crit_prompt())
abort_cleanup(FALSE);
else {
abort_cleanup(TRUE);
_write(STDERR, &message, sizeof(message));
}
exit(255);
}
void interrupt new_int_1Ch(void) {
asm {
add remainder,60648
adc WORD PTR milliseconds+0,54
adc WORD PTR milliseconds+2,0
}
return; /* From interrupt */
}
void intercept_int_1Ch(void) {
old_int_1Ch = getvect(0x1C);
setvect(0x1C, new_int_1Ch);
return;
}
unsigned long get_milliseconds(void) {
static unsigned long rv;
asm pushf;
asm cli;
rv = milliseconds;
asm popf;
return rv;
}
void main(void) {
int n;
milliseconds = 0;
printf("Sample program #20 - Demonstrates millisecond count using int 1Ch\n");
printf("Part of the PC Timing FAQ / Application notes\n");
printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
crit_err_intercept(); /* Trap critical errors */
setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */
intercept_int_1Ch(); /* Intercept int 1Ch */
printf("Press any key to display current millisecond count\n");
printf("Press <Esc> to exit\n\n");
do {
while (bioskey(1) == 0)
;
n = bioskey(0);
printf("Millisecond count is: %ld\n", get_milliseconds());
} while ((n & 0xFF) != 27);
abort_cleanup(TRUE);
exit(0);
}
-------------------------------- snip snip snip --------------------------------
## 10.13 NOTES ON MICROSOFT WINDOWS
I have no interest in Windoze, but I have received a few comments regarding
timing under Windoze which you may find useful.
{TOR} Tor Sjowall (tor@oslonett.no) said (slightly paraphrased):
> A regular 18Hz clock interrupt routine (int 8 or int 1Ch) in a DOS box under
> Windows works as usual as long as the DOS box has the focus, but when another
> window has the focus, the DOS box's timer tick interrupt rate slows down to
> one tick every 800 ms. The tick counter however is incremented as usual.
> This has driven me crazy...
>
> As far as I can gather from the documentation, this is the correct behaviour.
> The reason being that Windows wants to use as much of the CPU capacity as
> possible. Also I have not found any 'back door' into the 386 mode timer
> interrupt routine that will allow my program to catch the ticks.
>
> The RTC periodic interrupt sort-of works under Windows. The problem is that
> each DOS box has its own Virtual Machine, plus one for Windows itself. So
> all these VMs each get a simulated hardware interrupt from the real 386 mode
> interrupt handler. This works well enough, but the overhead is large.
> Actually, Windows has a real ugly wart here: If the hardware interrupt was
> enabled in the PIC before Windows was started, all the VMs under Windows will
> get a simulated hardware interrupt with I/O ports trapped, etc etc. If the
> interrupt was disabled on the PIC, only the VM that enabled the interrupt on
> the PIC gets the interrupt. There is a text on the Developer CD, 'The Tao of
> Interupts', that describes this in all its gory detail.
>
> The overhead is enormous: the V86 DOS interrupt has only 7% of the throughput
> of a native DOS interrupt routine. A 90Mhz Pentium is a good idea...
Thanks Tor for that information.
## 10.14 DOS FILE DATE AND TIME STAMPS
{TOR} also suggested this topic, as it is related to timing.
DOS's FAT (File Allocation Table) file system stores the date and time of last
modification of every file. Date and time values are each 16 bits (two bytes)
wide. In the directory structure, the date value is at offset 24 into the
directory entry, and the time value is at offset 22. In the findfirst/find-
next structure in the DTA, returned by DOS when findfirst or findnext are
requested, the date is also at offset 24 and the time at offset 22.
The file date word is constructed as follows.
F E D C B A 9 8 7 6 5 4 3 2 1 0
* * * * * * * . . . . . . . . . Year minus 1980 (range 0-119)
. . . . . . . * * * * . . . . . Month (range 1-12)
. . . . . . . . . . . * * * * * Day of month (range 1-31)
The file time word is constructed as follows.
F E D C B A 9 8 7 6 5 4 3 2 1 0
* * * * * . . . . . . . . . . . Hours (range 0-23, 24-hour format)
. . . . . * * * * * * . . . . . Minutes (range 0-59)
. . . . . . . . . . . * * * * * Seconds / 2 (range 0-29)
Note that the time is only stored with a resolution of two seconds, so the time
stamp on a file modified at 12:34:56 is the same as the time stamp on a file
modified at 12:34:57.
The date and time fields can be combined into an unsigned long value (date in
the hiword, time in the loword) and compared with other date/time fields in the
same format to see which file is newer or whether the date and time are the
same.
## 10.15 DOS AND THE DATE AND TIME
Under DOS, the current date and time is not stored in the DOS kernel, but is
provided as required, by the CLOCK$ driver. Whenever DOS wants to know the
date and time, it issues a 6-byte read request to the clock driver, which it
identifies via the CLOCK bit in the device attribute word. Traditionally the
driver name is "CLOCK$" but this is not required.
See section »» 3.3 for a replacement CLOCK$ driver that uses the AT's RTC.
The CLOCK$ driver supports reading and writing. Six bytes are always read and
written. These bytes encode the full date and time. The six bytes of data are
all in binary form. The structure is:
0 WORD Number of days since 1st January 1980 (0-up)
2 BYTE Minutes (0-59)
3 BYTE Hours (0-23)
4 BYTE Hundredths of seconds (0-99)
5 BYTE Seconds (0-59)
DOS will write to the CLOCK$ driver when int 21h, functions 2Bh or 2Dh (the DOS
set date and set time functions) are issued. It will read the CLOCK$ driver
when int 21h, functions 2Ah or 2Ch (DOS get date and get time functions) are
issued, or when it wants to know the date and time for timestamping a disk file.
The standard CLOCK$ driver supplied with DOS reads the date from the RTC on
initialisation (i.e. at reboot time), and converts this date to a count of days
elapsed since 1st January, 1980. It maintains the date internally in this form,
as this is the form used by DOS in the CLOCK$ read and write function calls.
The standard CLOCK$ driver uses the BIOS timer tick count variable to keep track
of the time of day. This variable is set up by the computer's BIOS from the RTC
time of day, as part of the power-on self-test (POST) procedure, so it is
correct when DOS boots.
The CLOCK$ driver issues BIOS interrupt 1Ah, function 0, Get Tick Count, every
time it is asked to supply the current date and time. This function returns a
flag in AL called the midnight flag. The flag is set by the BIOS int 8 handler
when the tick count variable wraps around from 1800AFh to 0, and is true the
_first_ time BIOS int 1Ah function 0 is called following a change of day.
After the BIOS has reported the flag true, it clears the flag, and it will only
be true again on the next change of day.
This is also described in section »» 4.2.
When the date and time are requested from the CLOCK$ driver, it calls the BIOS
function, checks the midnight flag and if set, increments its count of days
since 1st January 1980. It then converts the tick count into hours, minutes,
seconds, and hundredths of seconds. Of course the tick count has a resolution
of only 54.9254 ms, much more coarse than the 10ms resolution provided by the
hundredths of seconds value. I do not know what algorithm the CLOCK$ driver
uses to calculate the hours, minutes, seconds, and hundredths from the tick
count.
When the date and time are set, the CLOCK$ driver's days since 1980 count is
set, and the CLOCK$ driver presumably calculates an appropriate tick count value
and uses int 1Ah function 1 to set the tick count. I believe that the CLOCK$
driver also updates the RTC date and time, presumably through int 1Ah functions
3 and 5.
The DOS kernel contains the code to convert between days, months, and years,
and number of days since 1st January 1980. It can convert both ways, as the
date is both requested (int 21h function 2Ah) and set (int 21h function 2Bh)
in days, months, and years format.
So to summarise, at reboot, the BIOS sets up the tick count using the RTC time,
DOS boots and the CLOCK$ initialisation code calculates the number of days since
1 January 1980 from the RTC date and stores this internally. When the date and
time is requested from the CLOCK$ driver, it calls int 1Ah function 0, checks
the returned midnight flag and increments its day count if set, calculates the
hours, minutes, seconds, and hundredths values from the tick count, and returns
these values. When the date and time is set by writing to the CLOCK$ driver,
the driver updates its day count to the specified value, and presumably
calculates an appropriate tick count, sets it via int 1Ah function 1, sets the
RTC time via int 1Ah function 3, and calculates days, months, and years from
the day count and sets the RTC date via int 1Ah function 5.
If anyone has disassembled any DOS CLOCK$ drivers, please let me know what you
found out. (*) I will eventually do this anyway.
## 10.15.1 DOS DATE ROLLOVER BUGS
There are two problems related to the change of day under DOS's CLOCK$ driver.
The first is that int 1Ah, function 0, only returns the midnight flag set the
first time it is called following a change of day. If an application program
or TSR calls this function, and happens to call it after a change of day but
before the CLOCK$ driver calls it, the application or TSR will see the change
of day but the CLOCK$ driver will miss it. Therefore, no program should use
int 1Ah function 0. Also see section »» 4.2.
The second problem is that there is no way to tell how many midnights have
passed, since the midnight flag is just a flag, not a count. This problem
usually affects computers that run constantly but are unattended, where the
date and time may not be requested for a long time - more than 24 hours. Two
or more midnights may pass, but when the date and time is requested, the date
in the CLOCK$ driver is only incremented by one. I have heard that some BIOSes
actually implement the midnight flag as a count, and the CLOCK$ driver may
possibly respond to values other than 0 or 1 and update the date correctly,
but I don't know for sure. (*)
## 10.16 SIMULATING A VERTICAL RETRACE INTERRUPT
The aim of this technique is to provide an interrupt which is synchronised to
the screen refresh (i.e. vertical scan) so that certain functions that must be
performed during the vertical retrace period can be done via this interrupt,
in the background. For more details on this, see section »» 7.33.
Some video cards (notably EGA cards) are able to generate a vertical retrace
interrupt themselves, usually on IRQ2/9, but this facility is not standard on
VGA cards. The vertical retrace interrupt can be simulated using CTC channel 0.
Vertical retrace emulation is sometimes a hot topic on comp.os.msdos.programmer
and comp.lang.asm.x86, with many people interested in how it can be done, but
I (and my correspondent Anders Roar Nielsen, aroni@night.ping.dk) don't believe
that it is necessary in most applications. Retrace can be detected by polling,
the field time can be measured, and CTC channel 0 can be used to estimate where
the video circuitry is 'up to' (at the default divisor, there are at least three
field scans per CTC channel 0 wraparound). These techniques of maintaining code
synchronisation to the screen refresh, using the CTC, will generally have much
lower overhead and less impact on other aspects of the machine's performance.
The triple buffering technique, described in section »» 10.16.3, does rely on
vertical retrace interrupt simulation, however.
## 10.16.1 VERTICAL RETRACE INTERRUPT SIMULATION DESCRIPTION
The technique described in this section is based on an apparently well-known
algorithm. I saw it suggested by Tommy Marshall (tommym@oneworld.owt.com), in
his message <3vv8n1$j8g@paperboy.owt.com> of Sat 05 Aug 1995. I have enhanced
the algorithm to improve its performance under adverse conditions, and added
thorough documentation. In his posting, Tommy mentions that some demo source
code is available on his web site: http://www.owt.com/users/tommym/index.html.
Thanks to Anders Roar Nielsen (aroni@night.ping.dk) for his help with this
subject.
The following diagram will only make sense if viewed on a monospaced screen.
├───── 17088 ─────┼───── 17088 ─────┼───── 17088 ─────┼───── 17088 ─────┤
├──── 16968 ────┤ ├──── 16968 ────┤ ├──── 16968 ────┤ ├──── 16968 ────┤ ├──
┌┐ . ┌┐ . ┌┐ . ┌┐ . ┌┐
││ . ││ . ││ . ││ . ││
││ . ││ . ││ . ││ . ││
───┘└────────────────┘└────────────────┘└────────────────┘└────────────────┘└─
│ │ │ │ │ │ │ │ │ │ │ │ │ │
a b * c d e f g h i j k l m
The above diagram shows the retrace signal graphed against time (time is on the
horizontal axis). The numbers on the diagram are in units of one CTC clock
(0.8381 us). The values are as measured on my Tseng ET4000 W32i card operating
in standard 25-line, 80-column colour VGA text mode (720x400 pixels). The
pulses are the retrace indication from the video card, readable on the video
status input port. At point A, when the retrace indication becomes active,
the entire screen has been scanned and the electron beam is beginning its
vertical retrace - retracing its steps back to the top of the screen.
The retrace pulses are fairly short - in this case, only 76 CTC clocks, or
about 64 us long. The actual vertical retrace time (the time taken for the
electron beam to return to the top of the screen and start scanning the
displayable part of the picture) is much longer than the pulse indicates;
Klaus Hartnegg (klaus@mailserv.brain.uni-freiburg.de) reports that a typical
VGA vertical blanking period is about 2 ms. The visible part of the next
vertical scan starts a little later - say at point B. At point D, the scan
ends and the next retrace begins, ready for the next scan which starts at E.
During the retrace period, i.e. between point A and point B, it is safe to
modify certain parameters in the video subsystem, for example to perform page
flipping or some types of screen updates, without causing flicker or other
visible interference.
## 10.16.1.1 MEASURING THE FIELD TIME
The field time, i.e. the time span between the same edge of two adjacent retrace
pulses, can be measured by initialising CTC channel 0 in mode 2, waiting for a
rising (or falling) edge on the retrace indication, reading the count in CTC
channel 0, waiting for the next edge of the same type, and re-reading the count
in CTC channel 0, and calculating the number of CTC clocks elapsed between the
two samples. This is done with interrupts disabled. In this case, a time of
17088 CTC clocks was measured, with a fluctuation of +/- 1 CTC clock period.
The field period is this number multiplied by 0.8381 us (the CTC clock period).
In this case the field period is 14.321 ms. The field rate (number of fields
per second) is the reciprocal of the field time - in this case, about 69.8
fields per second, i.e. a vertical scan rate of about 69.8 Hz.
## 10.16.1.2 CONTROLLING THE CTC INTERRUPT
Having determined the field period, 17088 CTC clock periods, we can program CTC
channel 0 to give an interrupt a short time before the rising edge of the next
retrace pulse will be due. We wait for a start of retrace, i.e. point A, and
immediately reset and program CTC channel 0 for mode 2 with a count of slightly
less than 17088, so that it will generate an interrupt shortly before the start
of the next retrace, say at point C.
During normal operation, CTC channel 0 will issue an interrupt at point C. The
int 8 handler will loop, waiting for the start of the retrace (point D). When
this occurs, the interrupt handler resets and reprograms CTC channel 0, so that
it will interrupt again at point F. The important screen updates that must be
done during retrace, can now be performed. The interrupt handler then exits,
and the mainline gets execution until point F, at which point the CTC triggers
another interrupt and the cycle repeats as if from point C.
This technique assumes that the video mode does not change during execution and
is not reset. A video mode reset may cause the scanning to restart out of sync.
The interrupt will resynchronise in the latter case, but may lock interrupts for
an unusually long amount of time when doing so, as it may potentially remain in
the retrace wait loop for a long time.
## 10.16.1.3 SIGNIFICANCE OF THE SAFEMARGIN VALUE
The number of CTC clocks which are subtracted from the field period to give the
CTC count value is important. I will call it the SafeMargin value. The sample
program uses a default SafeMargin of 120 CTC clocks, or about 100 us.
The significance of the SafeMargin value is that it determines the maximum
interrupt latency that can be tolerated. This latency is made up of interrupt
acceptance delay (due to interrupts being locked out) and interrupt overhead
(e.g. overhead caused by EMM386).
If, for example, interrupts were locked out at point '*', by a CLI instruction
issued by the mainline or some code that was called by the mainline, then the
interrupt would be signalled (by the CTC) at point C, but would not be accepted
immediately. If the acceptance was delayed past point D, the start of the
retrace period, then the int 8 handler is going to see that the retrace has
already started. If this occurs, the int 8 handler cannot guarantee that there
is enough time for the screen manipulation, before the visible part of the scan
begins and the manipulation will cause visible interference.
Therefore, if interrupts are being locked out periodically by the mainline or
code called by the mainline, the SafeMargin value must be long enough to cover
the longest period for which interrupts will be locked out, plus any delays in
interrupt acceptance (EMM386 overhead), so that in the worst case, if interrupts
were locked out just before the CTC channel 0 interrupt was signalled, they will
be enabled in time for the interrupt to be accepted and the int 8 handler to be
entered and to check the retrace flag _before_ the retrace actually starts, so
that there is almost the entire retrace period available for the screen update.
The sample program allows the SafeMargin value to be set from the command line
via a decimal number which represents the number of CTC clocks (units of 0.8381
us) for the SafeMargin value. The default SafeMargin value is 120, giving a
safety margin of about 100 us including interrupt overhead.
If you use a short SafeMargin value, it is essential that no foreground code
locks out interrupts for any reasonable length of time. On the other hand, a
large SafeMargin value reduces the amount of time available for other processes
(i.e. the mainline), as a larger amount of time is spent in the loop in the int
8 handler, waiting for the start of retrace, between points C and D.
If SafeMargin is increased to more than about half of the vertical scan time,
the system falls apart, giving widely varying loop counts and a jumpy display.
I haven't bothered to try to figure out why this occurs, because half a field
period is normally at least 6 ms, so SafeMargin should never be anywhere near
this long, but I noticed that it can be fixed by moving the instructions that
prepare the CTC to accept its new count, back to just after the CTC count in
progress is read, so that the CTC is frozen during the wait-for-retrace loop.
## 10.16.1.4 OVERHEAD DUE TO LARGE SAFEMARGIN AND SCREEN UPDATE
Depending on the SafeMargin value you choose, you may also need to take into
account the time spent between points C and D, as it will take a chunk of
processing time, and operates with interrupts locked out. Remember that the
operations performed by the retrace function (screen updates, etc) are also
performed with interrupts locked out, so if they are extensive, this may have
a significant impact on latency for other interrupts.
For example, don't try to use this technique in a game that communicates via a
serial link or a modem (multi-player multi-computer games) unless you're using
a very low data rate, or carefully controlling the outgoing flow control lines
to prevent loss of incoming characters!
## 10.16.1.5 ENHANCED HANDLING OF MISSED RETRACE START
The above algorithm can be improved in cases where the start of retrace is
missed due to interrupt latency. First, if we keep track of the last reload
value that was programmed into the CTC, we can read the count in progress in
the CTC and subtract it from that value, to determine the number of CTC clocks
by which the interrupt was delayed. By subtracting our SafeMargin value, we
can determine how many CTC clocks into the retrace period we are. We may be
able to make use of a retrace interrupt even if it was delayed past the start
of retrace, if we know that there is still enough time to execute screen update
code before the start of the next visible scan. Also, we can correct for the
error by reprogramming CTC channel 0 even if we cannot get a timing reference
from the video subsystem. I will now explain these enhancements in detail,
using an example.
├──── 16968 ────┤ │
┌──┐ . ┌──┐ . ┌──┐
│ │ . │ .│ . │ │
│ │ . │ .│ . │ │
───┘ └──────────────┘ .└──────────────┘ └──
│ │││ │ │ │
a bcd e f g
Using the above diagram as an example, assume that the field period is 17088
clocks, SafeMargin is 120, and the visible scan starts at point E. The CTC
was programmed with a count of 16968. An interrupt is signalled at point A
but interrupts are locked out by the mainline or some code called by the
mainline. At point B, retrace has already started, but our interrupt routine
is still prevented from executing. Then interrupts are enabled at point C.
The interrupt handler starts immediately, but discovers that retrace has already
started. It reads the CTC count in progress at point D, and gets a value of,
say, 16728. At point A, the CTC reloaded with a count of 16968, so by
subtracting the count in progress from the last CTC count, i.e. 16968-16708,
which is 260 CTC clocks, we can calculate the number of CTC clocks between
point A and point D (point D being _now_). This value is the amount of time
by which the interrupt was delayed, and includes delay caused by interrupts
being locked out, and delay in the actual interrupt acceptance process, e.g.
delay caused by EMM386. I will call this value the Latency value.
We can then subtract SafeMargin (which is a fairly accurate estimate of the
time between points A and B) from the Latency value, to calculate the time
between point B (start of retrace) and point D (now). This gives a result of
140 clock cycles between B and D. If the start of visible scan at point E is
known to be, say, 1300 CTC clocks after point B (the start of retrace), we can
calculate how much time remains before the visible scan begins, by subtracting
the number we just calculated (140, the time between B and D) from the 1300
(the time between B and E), to get 1160 CTC clocks, the amount of time left
before the visible scan starts. If the screen update code is known to take
comfortably less than this amount of time, then it can still be executed.
If that was tricky, it gets trickier! If interrupt acceptance was delayed
so long that the interrupt routine executed after the _end_ of the retrace
pulse, it would not know that it had missed the pulse altogether, and would
sit in its wait loop, waiting for the start of retrace, for the entire
displayed field, until the _next_ retrace started! During this time, the
mainline could not execute, and interrupts would be locked out! We can detect
this, again by reading the CTC count in progress on entry to the int 8 handler
and determining how long the interrupt acceptance was delayed (i.e. the Latency
value). If the Latency is significantly longer than SafeMargin, we can assume
that we have at least missed the _start_ of the retrace, and possibly the end
as well.
When the interrupt is accepted within the SafeMargin period, we can wait for
the start of retrace, then resynchronise the CTC by resetting it and setting
the count to the field period minus SafeMargin (16968) again. But when we miss
the start of retrace, because interrupt acceptance was delayed for longer than
SafeMargin, we no longer have a video timing reference from which we can
resynchronise CTC channel 0. But since we know how long interrupt acceptance
was delayed (the measured Latency value), we can estimate a new count to
program into CTC channel 0, that will cause an interrupt at roughly the correct
point in the next cycle.
For example, in the above example, at point D we know that 260 clocks have
elapsed since point A when the interrupt was signalled by the CTC. We want
the next interrupt to be signalled at point F, which is SafeMargin clocks
before the start of the next retrace, at point G. The count to be programmed
into CTC channel 0 is therefore the field time minus the measured Latency.
CTC channel 0 is reset and programmed with a count of 17088 - 240, or 16848.
16848 CTC clocks later, it will generate the interrupt at point F.
This method is not 100% accurate, as there will be some delay in reading and
setting the CTC count, as well as some delay between the two. This would
result in the interrupts getting progressively later, if retraces were missed
repeatedly. As soon as an interrupt is accepted within SafeMargin, the CTC
is resynchronised from the retrace signal. I thought of a better method that
would have kept better synchronisation in these circumstances, and it should
have worked, but it didn't. Oh well.
## 10.16.1.6 OTHER NOTES
There may be special considerations for interlaced video modes. If you are
using these modes, you will probably already know enough to figure out whether
there will be any problems :-)
Also, because this technique intercepts int 8, the standard precautions as
described in section »» 5 should be taken, to ensure that the program is not
terminated without being able to clean up and restore the original int 8
handler and standard divisor on CTC channel 0.
I found it very interesting to run various programs from the DOS shell and see
the effect they have on interrupt latency. For example, on my 486DX2-66, the
background interrupt latency is typically 15 to 20 CTC clock cycles, and no
retraces are missed at SafeMargin = 120. My COMSPEC points to a file on a RAM
drive - if my command processor was on the hard disk, and was uncached, the
interrupt latency due to the DOS EXEC call that invokes the command processor
would probably make it impossible to determine the background latency.
The DOS EXEC call could be replaced with a delay loop, to determine the
background latency in this case (if you wanted to).
Listing a directory increases the longest int 8 latency slightly, but few if
any retraces are missed. But, running CHKDSK gives a longest latency of about
7000 to 8000 CTC clocks, and many missed retraces. With the SMARTDRV.SYS disk
cache installed, after CHKDSK has run once, it does not need to physically
access the hard drive again, and the maximum interrupt latency drops to about
80 CTC clocks, with no missed retraces (SafeMargin = 120).
## 10.16.2 SAMPLE PROGRAM: SIMULATING A VERTICAL RETRACE INTERRUPT
-------------------------------- snip snip snip --------------------------------
NAME SAMPLE21
; Sample program #21
; Demonstrates a simulated vertical retrace interrupt
; Part of the PC Timing FAQ / Application notes
; By K. Heidenstrom (kheidens@actrix.gen.nz)
;
; This program assembles into SAMPLE21.COM, a program which implements an
; simulated vertical retrace interrupt using CTC channel 0. It installs its
; interrupt handler, and shells to DOS, allowing other programs to be run
; while its interrupt handler is installed. The interrupt handler causes the
; screen text to move up and down, in a seasickness-inducing fashion.
;
; This program requires a VGA card able to operate in video mode 3 (80x25,
; colour mode) but does not check that this is present.
;
; Save this file to SAMPLE21.ASM and assemble with:
; masm SAMPLE21;
; link SAMPLE21;
; exe2bin SAMPLE21.exe SAMPLE21.com
; or
; tasm SAMPLE21;
; tlink /t SAMPLE21;
;
; The techniques used in this program cannot safely be used in a TSR or a
; program that shells to DOS in a general way to run any DOS program. This
; technique is intended to be used as part of an application, where the
; behaviour of the 'foreground' code is known and controlled as much as
; possible. Though this program does shell to DOS, it is not intended to be
; used to run all types of programs. The shell to DOS feature is just to
; demonstrate that the screen updates are in fact being done under interrupt.
;
; This program can be assembled with or without the performance monitoring and
; reporting capability. Set the REPORT conditional to 0 for no performance
; monitoring and reporting, or to 1 for performance monitoring and reporting.
; Additional code in the int 8 handler is enabled if REPORT is enabled.
; The performance and behaviour monitoring functions will often be useless in
; production code.
REPORT = 1 ; Enable for report stuff
Code SEGMENT
ASSUME cs:Code,ds:Code
ORG 100h
Main: jmp Main2
SignOnMsg DB 13,10,"SAMPLE21 -- Demonstrates simulated vertical retrace interrupt",13,10
DB "Part of the PC Timing FAQ / Application notes",13,10
DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10,13,10
DB "Usage: SAMPLE21 [Safety-margin]",13,10,13,10
DB "This program assumes, but does not check for, a VGA card in 80x25 mode",13,10,13,10
DB "Type EXIT at the DOS prompt to quit this program",13,10,13,10,"$"
ComspecMsg DB "SAMPLE21: Can't locate COMSPEC in environment",13,10,"$"
IF REPORT
Msg0 DB 13,10," Chosen safety margin: $"
Msg1 DB " CTC clocks",13,10," Measured field time: $"
Msg2 DB " CTC clocks",13,10," Total retraces: $"
Msg3 DB 13,10,"Missed retrace starts: $"
Msg4 DB 13,10,"Longest int 8 latency: $"
Msg5 DB " CTC clocks",13,10," Longest retrace wait: $"
Msg6 DB " loops",13,10,"Shortest retrace wait: $"
Msg7 DB " loops",13,10,"$"
ENDIF
ComSpecTxt0 DB "COMSPEC=" ; Text to find COMSPEC in environment
ComSpecTxtL = $ - ComSpecTxt0 ; Length of same
ComspecPtr DW 0 ; Pointer to COMSPEC in environment
ALIGN 2
ExecParmBlock: ; EXEC parameter block
EnvirSeg DW 0 ; Segment-paragraph of environment
DW ShellCommand ; Pointer to command line
SetToCS1 DW 0 ; Segment part for above
DW 5Ch ; Let it use our FCBs
SetToCS2 DW 0 ; Segment part again
DW 6Ch ; Ditto
SetToCS3 DW 0 ; Ditto
ShellCommand DB 0,13 ; Command tail length and contents
IF REPORT
ReportTbl DW MsgPrint,Msg0
DW PrintDec,SafeMargin
DW MsgPrint,Msg1
DW PrintDec,FieldPeriod
DW MsgPrint,Msg2
DW PrintDec,Retraces
DW MsgPrint,Msg3
DW PrintDec,MissedStarts
DW MsgPrint,Msg4
DW PrintDec,MaxLatency
DW MsgPrint,Msg5
DW PrintDec,Longest
DW MsgPrint,Msg6
DW PrintDec,Shortest
DW MsgPrint,Msg7
DW Continue,0
ENDIF
SafeMargin DW 120 ; Interrupt 100us early (default)
FieldPeriod DW 0 ; Number of CTC clocks in each field
; (frames per second = 1,193,181.66666... / FieldPeriod)
LastCTC DW 0 ; Last CTC count programmed
Latency DW 0 ; Actual latency for this interrupt
Int8Sched DW 0 ; Scheduler for calling BIOS int 8
IF REPORT
First DW 1 ; Flag whether first retrace
Retraces DW 0 ; Count of retraces
MissedStarts DW 0 ; Count of missed retrace starts
MaxLatency DW 0 ; Worst int 8 delay (CTC clocks)
Longest DW 0 ; Longest retrace wait (loops)
Shortest DW 0FFFFh ; Shortest retrace wait (loops)
ENDIF
Cycler DW 0 ; Cycle control variable
; The sinewave table was created using the following GW-BASIC program using a
; number of entries of 64, range of values of 16, and centre offset of 7.5.
; The program generated one '16' in the middle of the '15' values, but I just
; manually fixed this to 15. A value of 16 causes the screen to jump.
;
;10 PRINT"This program will generate a sinewave table. The table is written to"
;20 PRINT"a disk file called SINE.DMP. The file is in text form, and contains"
;30 PRINT"one line per entry, in ASCII decimal representation. All entries are"
;40 PRINT"integers. Parameters required are: number of entries in table, range"
;50 PRINT"of values (peak to peak), and centre offset. One cycle of sine wave"
;60 PRINT"is written.":PRINT:INPUT"Number of entries in table :",NE#
;70 INPUT"Peak to peak value range :",PP#:INPUT"Zero offset :",ZO#
;80 OPEN "SINE.DMP" FOR OUTPUT AS#1 : A# = 0 : I# = 6.283185307179586#/NE#
;90 FOR P = 1 TO NE# : S# = SIN(A#) : V = INT((S# * PP# / 2) + ZO# + .5#)
;100 PRINT #1,V : A# = A# + I# : NEXT : CLOSE #1 : SYSTEM
CycleTbl DB 8,8,9,10,11,11,12,13,13,14,14,15,15,15,15,15
DB 15,15,15,15,15,15,14,14,13,13,12,11,11,10,9,8
DB 7,7,6,5,4,4,3,2,2,1,1,0,0,0,0,0
DB 0,0,0,0,0,0,1,1,2,2,3,4,4,5,6,7
Main2 PROC near
cld ; Upwards string direction
mov si,81h ; Command tail
Loop1: lodsb ; Get character
cmp al,13 ; C/R yet?
je NoParam ; If so
cmp al," " ; Whitespace?
jbe Loop1 ; Loop if so
; Parse decimal number parameter to replace default SafeMargin value
xor bx,bx ; Clear calculated value
ReadNumLp: sub al,"0" ; Convert "0"-"9" to 0-9
cmp al,9 ; Check for valid char
ja ReadNumFin ; If not, terminator
cbw ; Zero AH
xchg ax,bx ; New digit to BL, old total to AX
mov dx,10 ; Ten to unused register
mul dx ; Multiply old value by ten
add bx,ax ; Add to new digit
lodsb ; Read char from command tail
jmp SHORT ReadNumLp ; Loop for more
ReadNumFin: mov [SafeMargin],bx ; Store adjustment value
NoParam: mov es,[ds:2Ch] ; Get segment of environment
mov [EnvirSeg],es ; Set up for command processor
xor di,di ; Start at start of environment
ScanEnvLoop: mov si,OFFSET ComSpecTxt0 ; Point at 'COMSPEC='
mov cx,ComSpecTxtL ; Get length to compare
push di ; Keep pointer to start
repe cmpsb ; Compare to 'COMSPEC='
pop cx ; Restore pointer to start
je GotComspec ; If found it
mov di,cx ; Go to start of entry again
mov cx,8000h ; Maximum length to scan
xor al,al ; Null terminator to scan for
repne scasb ; Scan for null terminator
jne EnvirError ; If error in environment
cmp BYTE PTR es:[di],0 ; Final entry in environment?
jne ScanEnvLoop ; If not, keep looking
EnvirError: mov dx,OFFSET ComspecMsg ; Point to message
mov ah,9
int 21h ; Display it
mov ax,4C01h ; Errorlevel 1
int 21h
int 20h ; In case DOS-1 (!)
GotComspec: mov [ComspecPtr],di ; Store offset into environment
mov [SetToCS1],cs ; Set up segment-paragraphs in EXEC
mov [SetToCS2],cs ; parameter block for command
mov [SetToCS3],cs ; interpreter
; Relocate stack and shrink memory allocation
push cs
pop es ; ES to Code
mov sp,OFFSET StackTop ; Relocate stack
mov bx,OFFSET FreeSpace+15 ; Account for partial paragraph
mov cl,4 ; Shift count
shr bx,cl ; Shift to paragraph count
mov ah,4Ah
int 21h ; Shrink memory to minimum necessary
; First, set the VGA card to the required mode. It must be a colour mode,
; otherwise the retrace flag appears in a different I/O port and the code
; will fail. Any required mode tweaking would be done here, too.
mov ax,3 ; Screen mode
int 10h ; Set screen mode
mov dx,OFFSET SignOnMsg ; Point to sign-on message
mov ah,9
int 21h ; Display it
; Set CTC channel 0 for a known mode and reload value - mode 2, 65536.
cli ; No interrupts here please
mov al,00110100b ; Channel 0, lobyte/hibyte, mode 2, bin
out 43h,al ; Prepare channel 0 for new divisor
jmp SHORT $+2 ; Short delay
xor al,al ; Divisor is 0 (65536)
out 40h,al ; Write lobyte of divisor
jmp SHORT $+2 ; Short delay
out 40h,al ; Write hibyte of divisor
; Time the number of CTC clocks between two retraces
call StampRetrace ; Load the processor cache
call StampRetrace ; Wait for start of retrace, read CTC
mov bx,ax ; Keep it
call StampRetrace ; Do the same again
sub ax,bx ; Calculate difference
mov [FieldPeriod],ax ; Store retrace period (in CTC clocks)
; Calculate the value to be programmed into CTC channel 0 from now on
sub ax,[SafeMargin] ; Subtract the desired safety margin
mov [LastCTC],ax ; Store as last programmed value
; Program the timer to interrupt just before the next retrace starts
xchg ax,dx ; To DX
mov al,00110100b ; Channel 0, lobyte/hibyte, mode 2, bin
out 43h,al ; Prepare channel 0 for new divisor
jmp SHORT $+2 ; Short delay
mov al,dl ; Lobyte of divisor
out 40h,al ; Write lobyte of divisor
jmp SHORT $+2 ; Short delay
mov al,dh ; Hibyte of divisor
out 40h,al ; Write hibyte of divisor
sti
mov ax,3508h
int 21h ; Get int 8 vector
mov [Old8Ofs],bx ; Store offset
mov [Old8Seg],es ; Store segment
mov dx,OFFSET New8 ; Point to new handler
mov ax,2508h
int 21h ; Set vector
push cs
pop es ; ES back to Code
; Now execute the command processor
mov bx,OFFSET ExecParmBlock ; Point to EXEC parameter block
mov dx,[ComspecPtr] ; Get offset to command specification
mov ds,[EnvirSeg] ; Get segment of environment
ASSUME ds:nothing
mov ax,4B00h
int 21h ; Execute command interpreter
push cs
pop ss ; Restore SS
mov sp,OFFSET StackTop ; Reset stack
push cs
pop ds ; Restore DS
ASSUME ds:Code
; Restore VGA CRTC register 8 to its default value
mov dx,3D4h ; Address VGA CRTC
mov ax,8 ; Register number and value (0)
out dx,ax ; Restore it
; Restore normal mode and divisor in CTC channel 0
cli ; No interrupts around this bit
mov al,00110110b ; Channel 0, lobyte/hibyte, mode 3
out 43h,al ; Prepare channel 0 for new divisor
jmp SHORT $+2 ; Short delay
xor al,al ; Divisor is 0 (65536)
out 40h,al ; Write lobyte of divisor
jmp SHORT $+2 ; Short delay
out 40h,al ; Write hibyte of divisor
sti ; Interrupts are OK now
lds dx, [DWORD PTR Old8Ofs] ; Get old int 8 handler
ASSUME ds:nothing
mov ax,2508h
int 21h ; Restore int 8 vector
push cs
pop ds ; DS back to Code
ASSUME ds:Code
; Generate report if REPORT conditional enabled
IF REPORT
cld ; Just make sure
mov si,OFFSET ReportTbl
ReportLp: lodsw ; Handler address
xchg ax,cx ; To CX
lodsw ; Parameter
xchg ax,bx ; to BX
mov ax,[bx] ; Get value (if applicable)
mov dx,bx ; Pointer to DX
call cx ; Call handler
jmp SHORT ReportLp ; Loop
Continue: pop ax ; Fix up stack
ENDIF
mov ax,4C00h
int 21h
Main2 ENDP
; This function waits for the start of a vertical retrace then reads the count
; in progress in CTC channel 0. It assumes a VGA card, running in a colour
; mode. It also assumes CTC channel 0 is operating in lobyte-hibyte access
; mode and operating mode 2, and returns the count in AX, converted to an
; up-count. It first waits for any retrace currently in progress to end, then
; waits for the next retrace to start and immediately reads the CTC count.
; This function must be called with interrupts disabled. Destroys AX and DX.
StampRetrace PROC near
mov dx,3DAh ; VGA status port in colour modes
WaitRetr1: in al,dx ; Read status
test al,00001000b ; Check retrace flag
jnz WaitRetr1 ; If set, we are already in a retrace
WaitRetr2: in al,dx ; Read status
test al,00001000b ; Check retrace flag
jz WaitRetr2 ; If clear, keep waiting for retrace
xor al,al ; Command to latch channel 0
out 43h,al
jmp SHORT $+2 ; Short delay
in al,40h ; Read lobyte of count in progress
jmp SHORT $+2 ; Short delay
mov ah,al ; Keep it in AH
in al,40h ; Read hibyte of count in progress
xchg al,ah ; To correct registers
neg ax ; Convert to up-count
ret ; Return in AX
StampRetrace ENDP
; This function prints AX in ASCII decimal representation. Output is via DOS
; function 2. AX, BX, CX, and DX are all destroyed.
PrintDec PROC near
xor cx,cx ; Zero digit counter
PrintDec1: xor dx,dx ; Clear high word of value in DX|AX
mov bx,10 ; Base
div bx ; Divide by 10
add dl,"0" ; DL is remainder, convert to ASCII
push dx ; Store on stack
inc cx ; Increment char counter
test ax,ax ; Any more digits left?
jnz PrintDec1 ; If so, loop
PrintDec2: pop dx ; Get char back
mov ah,2 ; Print char
int 21h ; Call DOS
loop PrintDec2 ; Loop for all chars
ret ; Done
PrintDec ENDP
MsgPrint PROC near ; Print message pointed to by DX
mov ah,9
int 21h ; Print message ('$' terminated)
ret
MsgPrint ENDP
; The following function is the replacement int 8 handler. There is a lot of
; conditional code that is enabled by the REPORT conditional. The version
; with reporting is very instructive, and useful during development, but you
; may prefer to base production code on the version without the performance
; monitoring code.
ASSUME ds:nothing
New8 PROC far ; New int 8 handler
cli ; Make sure
push ds
push cs
pop ds ; Address this segment with DS
ASSUME ds:Code
push dx
push cx
push bx
push ax
pushf
cld ; Ensure DF is known
; Read count in progress in the CTC to CX
xor al,al ; Command to latch channel 0
out 43h,al
jmp SHORT $+2 ; Short delay
in al,40h ; Read lobyte of count in progress
jmp SHORT $+2 ; Short delay
xchg ax,cx ; To CL
in al,40h ; Read hibyte of count in progress
mov ch,al ; To CH - now CX = count in progress
; Now have count in progress, in CX. Calculate the latency on this interrupt
; invocation. This can be determined from the reload value last programmed
; into the CTC (which is stored in LastCTC). The difference between LastCTC
; and the count in progress, is the latency. This value is left in CX.
; If reporting, update the MaxLatency variable if appropriate.
neg cx ; Convert count in progress to negative
add cx,[LastCTC] ; Now have latency for this interrupt
mov [Latency],cx ; Store as measured latency
IF REPORT
cmp cx,[MaxLatency] ; Update MaxLatency
jbe NotWorse ; If not exceeded current value
mov [MaxLatency],cx ; If exceeded, update
ENDIF
; Check for this interrupt handler being entered too late. This occurs if a
; retrace was already in progress when the interrupt routine was entered, or
; if the measured latency is significantly greater than the SafeMargin value
; (at least, say, 10 or 20 CTC clocks later, to allow for timing alignment
; errors - I have chosen 20 CTC clocks; anything less than this will always
; be picked up by the retrace being already active).
;
; If this occurs, the interrupt was delayed longer than the SafeMargin value,
; and the start of the retrace interval (and possibly the whole retrace pulse)
; has been missed.
;
; The logic is that either the interrupt was accepted in time, in which case
; we will wait for the start of retrace and reset the CTC with the correct
; delay again, or the interrupt was delayed past the start of retrace (and
; possibly even past the end of retrace!) In this case, we must find out
; how 'late' we are, not wait for the start of retrace (as it has already
; started and may even have finished), and program the CTC with an adjusted
; delay (adjusted downwards), so that the next interrupt will be signalled
; on schedule.
; This technique does not resynchronise the CTC interrupt to the video system,
; and does not include compensation for the delays in the code, so if retraces
; are missed repeatedly, the timing of the interrupts is likely to drift.
; After the first successful interrupt entry, however, the CTC will be
; resynchronised to the video retrace.
NotWorse: xor bx,bx ; Zero loop counter / flag for later
mov dx,3DAh ; VGA status port
in al,dx ; Get status
test al,00001000b ; Check for retrace
jnz MissedRetrace ; If active, we missed the start
mov ax,[SafeMargin] ; Get ideal interrupt acceptance delay
add ax,20 ; Get maximum expected safety window
cmp cx,ax ; Measured interrupt acceptance delay
jae MissedRetrace ; Oh dear, we missed it completely!
; The latency (interrupt acceptance delay) was comfortably smaller than
; SafeMargin, and retrace is not active, so presumably the interrupt was
; accepted within the safe period. We can now wait for the retrace to
; start, then reprogram the CTC with the standard delay (from FieldPeriod
; minus SafeMargin). In report mode, count the loops while waiting.
Retrace1: IF REPORT
cmp bx,0FFFFh ; Check for overflow
adc bx,0 ; Increment if not
ENDIF
in al,dx ; Read status
test al,00001000b ; Check retrace flag
jz Retrace1 ; If clear, keep waiting for retrace
; We have just successfully completed the wait-for-retrace loop. If in report
; mode, update longest and shortest wait times but only if this is not the
; first retrace.
IF REPORT
cmp [First],0 ; Is it the first retrace?
jnz IsFirst ; If so
cmp bx,[Longest] ; Longer than longest?
jbe NotLonger ; If not
mov [Longest],bx ; If so
NotLonger: cmp bx,[Shortest] ; Shorter than shortest?
jae NotShorter ; If not
mov [Shortest],bx ; If so
IsFirst: mov [First],0 ; Reset First flag if set
NotShorter: ELSE
inc bx ; Flag that retrace was safe
ENDIF
mov cx,[FieldPeriod] ; Total field time minus safe margin
sub cx,[SafeMargin] ; Prepare value to load into CTC
jmp SHORT ResetCTC ; Go to set up CTC
; We missed the start of retrace because the interrupt was delayed, probably
; by some foreground code locking interrupts out for a long time. This can't
; be helped now, but we must adjust the value programmed into the CTC to
; trigger the next interrupt, so that it will interrupt proportionally sooner,
; otherwise we will miss the next retrace, etc.
; Calculate the new value to be programmed into the CTC. This is simply the
; retrace period (FieldPeriod) minus the measured latency, which is already in
; CX from earlier calculations. This gives an adjusted value to load into the
; CTC for the next delay, so that it will interrupt at roughly the correct
; point next time.
MissedRetrace: IF REPORT
inc [MissedStarts] ; Flag we missed a retrace start
ENDIF
neg cx ; Get minus interrupt acceptance delay
add cx,[FieldPeriod] ; Get adjusted CTC load value
; At this point, we have either missed the start of retrace and calculated a
; reduced value to load into the CTC for the next delay, or we have just had
; the start of retrace and have the standard value (FieldPeriod - SafeMargin)
; to load into CTC channel 0. CX contains the value to be loaded into the CTC
; to determine the delay from now until the next interrupt is signalled.
; Reset and restart the CTC using this value.
ResetCTC: mov [LastCTC],cx ; Store as last programmed value
mov al,00110100b ; Channel 0, lobyte/hibyte, mode 2, bin
out 43h,al ; Prepare channel 0 for new divisor
jmp SHORT $+2 ; Short delay
xchg ax,cx ; Get CTC count value
out 40h,al ; Write lobyte of divisor
jmp SHORT $+2 ; Short delay
mov al,ah ; Hibyte of divisor
out 40h,al ; Write hibyte of divisor
; Set carry flag according to whether retrace missed, and call RetraceFunc.
; BX was cleared before the test for retrace already started, so if retrace
; had already started (i.e. retrace start missed), BX will still be zero.
; If not, BX will be at least 1, as it is incremented in the wait loop (if
; reporting is enabled) or explicitly (if reporting is not enabled).
cmp bx,1 ; Set carry if retrace had started
call RetraceFunc ; Do retrace stuff
; Increment retrace count (but not above 0FFFFh)
IF REPORT
cmp [Retraces],0FFFFh ; Check for overflow
adc [Retraces],0 ; Increment if not
ENDIF
; Either chain to BIOS int 8 handler, or send EOI to PIC and return from
; interrupt. The decision is made via the Int8Sched variable, which is
; incremented by the number of CTC clocks in each field (FieldPeriod).
; If it carries, the BIOS int 8 handler is called. Otherwise, we just send
; an EOI and return from interrupt.
; In production code, this logic could be modified to remove the Int8Sched and
; the conditional chain to the BIOS int 8 handler, and always send the EOI and
; return. If this is done, the system time will stop updating while the handler
; is installed. There is little to be gained by doing this, as the interrupt
; rate is not very high, so I suggest leaving the chaining code intact.
mov ax,[FieldPeriod] ; Get number of CTC clocks elapsed
add [Int8Sched],ax ; Add into BIOS int 8 scheduler variable
cli ; Don't allow stack growth
jc CallOld8 ; If it carried, chain to the BIOS
mov al,20h ; EOI command
out 20h,al ; Send to primary PIC
popf ; Restore registers
pop ax
pop bx
pop cx
pop dx
pop ds
ASSUME ds:nothing
iret
CallOld8: popf ; Restore registers
pop ax
pop bx
pop cx
pop dx
pop ds
DB 0EAh ; JMP xxxx:xxxx
Old8Ofs DW 0 ; Offset of BIOS int 8 handler
Old8Seg DW 0 ; Segment of BIOS int 8 handler
New8 ENDP
; RetraceFunc is called by the replacement int 8 handler on every retrace, just
; shortly after the start of retrace (unless the start of retrace was missed,
; see shortly). On entry, the main segment is addressable via DS, the direction
; flag is clear, and interrupts are disabled - they may be enabled within
; RetraceFunc, but since IRQ0 (the highest priority interrupt) is in progress,
; no other interrupt sources will get through anyway, so there is no point in
; issuing an STI. The flags and the four scratchpad registers (AX, BX, CX, and
; DX) may be destroyed, but any other registers must be preserved - specifically
; BP, SI, DI, and ES must be preserved. The int 8 handler does not perform a
; stack switch, so stack usage must be kept to a minimum. On entry, the carry
; flag indicates whether a full retrace period is available, and the Latency
; variable can be used to determine how much time remains if the full period
; is not available.
; The timer interrupt normally triggers a certain time (set by SafeMargin)
; prior to the start of a retrace, giving a safety margin in case interrupts
; are locked out and the timer interrupt is not actioned immediately when it
; is signalled by CTC channel 0. If interrupts are locked out for more than
; the safety margin period, the timer interrupt may be delayed until after
; the start of retrace, possibly even until after the end of the retrace pulse.
; The int 8 handler detects this condition, and sets the carry flag on entry to
; RetraceFunc if this occurred. Normally, carry will be clear on entry to
; RetraceFunc.
; This function may make use of the Latency variable, which contains the
; number of CTC clocks by which the current interrupt was delayed. Typically
; this will be in the order of 15 to 20, but it will be much higher if this
; interrupt entry was delayed by interrupts being disabled by foreground
; code. If the time between the start of retrace and the start of the next
; visible scan is known, it is possible to use the Latency variable to find
; the amount of time remaining before the visible scan starts.
; See the explanatory text for this program for more details.
;
; This function would be changed to a user-specific function.
;
; This function must obey the normal guidelines for hardware interrupt handlers,
; for example it must not try to call any DOS functions. Some BIOS functions
; are generally safe to call from hardware interrupt handlers, but in general,
; special operations such as page flipping, palette changing, font programming,
; etc should be done at a hardware level, and the mainline code should be aware
; that these operations are being done 'in the background' if there may be some
; interaction.
RetraceFunc PROC near
pushf ; Preserve carry flag
mov bx,[Cycler] ; Get current point
inc bx ; Bump offset
and bx,3Fh ; Mask
mov [Cycler],bx ; Store back
popf ; Restore carry flag
jc DontUpdate ; If missed start of retrace
mov ah,[CycleTbl+bx] ; Get position for this cycle step
mov dx,3D4h ; Address VGA CRTC
mov al,8 ; Register number
out dx,ax ; Set vertical start position
DontUpdate: ret
RetraceFunc ENDP
DB 256 DUP(?) ; Stack space
StackTop = $ ; Top of stack point
FreeSpace = $ ; End of memory required
Code ENDS
END Main
-------------------------------- snip snip snip --------------------------------
## 10.16.3 DOUBLE AND TRIPLE BUFFERING
Thanks to Paul Ross (pa-ross@uwe.ac.uk) for his help with this subject.
Double buffering uses two screen buffers. While one buffer is being displayed,
the other buffer is being updated with data for the next frame. The video card
is told to change to the other buffer only during a vertical retrace, so the
animation is smooth and flicker-free.
The general flow is as follows:
while (1) {
Generate next frame using currently non-displayed buffer;
Wait for vertical retrace to begin;
Tell video card to swap to other buffer;
}
There is no requirement for a vertical retrace interrupt, because the software
simply creates a buffer of data then waits until the retrace starts, then
flips pages and starts creating the next buffer, and so on.
If one frame of picture data can be generated in less than one vertical scan,
the buffer alternates every retrace, and the frame update rate is equal to the
vertical scan rate, i.e. 70 frames per second (or whatever). The software
wastes time waiting for the vertical retrace, but if the software is still able
to keep up with the maximum frame display rate of the video hardware, this is
not a problem. But if it takes slightly longer than one frame to generate the
next frame, a lot of time is wasted in the loop waiting for retrace.
These diagrams may make more sense (if viewed on a monospaced display).
The first diagram shows the software able to generate a new frame more
quickly than the video card's frame rate:
Retraces: ! ! ! !
Software: 1111111111wwwwwf2222222222wwwwwf1111111111wwwwwf22222...
Display: 222222222222222 111111111111111 222222222222222 11111...
Key: 1 = Generating data for, or displaying, buffer 1
2 = Generating data for, or displaying, buffer 2
w = Waiting for vertical retrace
f = Flipping pages on video card
The above diagram showed the buffers alternating every retrace, giving the
maximum displayable frame update rate. The next diagram shows what happens
with double buffering when it takes longer than one displayed frame for the
software to create data for the next frame:
Retraces: ! ! ! ! ! ! !
Software: 1111111111wwwwwf2222222222wwwwwf1111111111wwwwwf22222...
Display: 2222222 2222222 1111111 1111111 2222222 2222222 11111...
As you can see, if the software is too slow to keep up with the frame rate of
the video card (as is often the case), the same frame will be displayed twice
or three times (or whatever), while the software is creating the next frame.
Once the software has a new frame ready, it then starts _waiting for the start
of the next frame_, wasting up to nearly a whole frame time doing nothing.
If the code takes, say, 1.3 frames to generate a picture, it will always flip
pages every two frames, because it can't do anything while waiting for the
retrace. So the screen updates are always evenly spaced (assuming that each
frame takes the same amount of time to generate), but if you could use that
waiting time, you could actually flip pages on two out of every three frames,
like this:
Retraces: ! ! ! ! ! ! !
Software: 111111111122222f2222233f3333333f111111111222222222333...
Display: 3333333 3333333 1111111 2222222 3333333 3333333 11111...
Comments: ^1ready ^2ready ^3ready ^1ready
So you get the page flips spaced unevenly, but the frame rate goes up.
This is called triple buffering, and it helps by allowing you to get a higher
frame rate but with uneven frame timing. For example if it takes 1.3 frames
of time to generate a new frame of data, with double buffering you would spend
0.7 frames every two frames (i.e. 35% of the processor time) waiting for the
next retrace, so you would get one new frame of data every two scans, i.e. 35
fps. But with triple buffering, you could start creating a new frame during
that 0.7 of a frame, so that it could be ready sooner. So you would get
unevenly spaced frames, but a higher frame rate.
To implement triple buffering, first of all you must use a video mode where
three (or more) pages are available. Then to detect retrace, you can either
poll the card (but remember the retrace pulse may be only about 64 us wide!),
or use some method based on polling the CTC, or use the vertical retrace
interrupt or an emulated vertical retrace interrupt.
Using the vertical retrace interrupt or emulated vertical retrace interrupt
method, your mainline (frame data generation) must keep some variable to show
which frame contains the most recent valid data, and a flag to say that a new
frame is available, which could be combined with the other variable. The
interrupt routine, which is triggered every retrace, would then check to see
if a new frame is available, and if so, flip pages to enable the most recently
updated page to be displayed.
So the mainline code flow would be something like:
Set newframe variable to -1;
Set workframe variable to 0;
while (1) {
Generate frame in buffer specified by workframe variable;
Set newframe variable to workframe variable;
Increment workframe variable modulo 3 (0,1,2,0,1,2,0...);
}
And the vertical retrace interrupt handler flow would be:
If newframe variable is not -1 { /* Only flip if new data available */
Flip displayed page to be equal to newframe variable;
set newframe variable to -1;
}
## 11 QUESTIONS AND ANSWERS
Well, since this is supposed to be a FAQ, I suppose I should include some
frequently asked questions and my answers to them :-) Most of these questions
are from Usenet newsgroups alt.msdos.programmer, comp.os.msdos.programmer,
comp.lang.asm.x86, and related newsgroups. I have paraphrased most of them.
## 11.1 TIMING ACCURACY
----------
> What is the inherent inaccuracy in DOS's timekeeping and how can it be
> avoided in an application where long term time accuracy is important?
There are 1,573,042.24 ticks in a day, but when the BIOS was written, the
1.19318166666... MHz frequency was approximated to 1.193180 MHz, so the BIOS
writers used 1,573,040 (001800B0 hex) ticks per day. This contributes a
'by-design' error of 1.42166 parts per million, but this is swamped by the
error due to initial accuracy, temperature stability, and long term drift in
the 14.31818 MHz system clock crystal, which is 5 ppm for a good quality
crystal, and maybe 50 ppm for the crap ones that are often found in cheap PCs.
One solution would be to write a DOS device driver for the CLOCK$ device, which
accesses the RTC (either directly, or through the BIOS functions), so that the
DOS time no longer relates to the time maintained by the BIOS in the timer tick
count variable.
However, the errors (initial, temperature, and long term) in the clock frequency
of the RTC are probably going to be unacceptable also, unless your motherboard
has a trimmer capacitor to fine-tune the oscillator frequency, and you have
_lots_ of time to spend adjusting it :-) Also the RTC only has a resolution
of one second.
If you really need high accuracy, there are several approaches.
■ Measure the accuracy over a one day or one week period and install an
adjustment factor in the software to compensate for the initial frequency
error (has to be done individually for each machine that will run the
software). This method doesn't help against temperature and long-term
drift.
■ Install a more accurate crystal or a high quality crystal oscillator module.
■ Use an external frequency source - either a clock controlled from a high
quality crystal in a temperature controlled environment (crystal oven) or
something derived from an external clock source (such as the mains frequency,
or perhaps radio time signals?), into an input such as the parallel port ACK
line which can generate an interrupt.
----------
> I want to implement a 10 millisecond clock, i.e. an interrupt every 10 ms.
> The PIT clock is 1.19318 Mhz, so a count of 11931 will give an interrupt
> at a rate of 11931/1193180 = 9.99933 ms. Using a divisor of 11931, I counted
> interrupts over a long period and got 9.99849 ms per tick.
The PIT clock is 14.31818/12, or 1.19318166666.... MHz. The absolute accuracy
is normally better than +/- 100 ppm, often under +/- 10 ppm, depending on the
accuracy of the crystal. Modern motherboards may not use accurate crystals,
because there is not normally any reason to - the RTC determines the long-term
accuracy and this is is clocked separately and read on every reboot. Try again
using the correct value for the timer clock frequency - this should give a
closer result, but you may not be able to get the accuracy you need.
> I tried a count of 11932. This should give a tick interval of greater
> than 10 ms, but instead, I get the 9.99933 ms tick interval I expected
> with a count of 11931. Even worse, all of this happens only on some
> machines; others work as expected.
Clock frequencies vary from machine to machine, also with age and temperature,
again depending on the quality of the crystal used. If the program just has
to run on one machine, and its clock frequency is slightly off but at least
stable, you may be able to calculate the actual clock frequency and modify
the program to accommodate it.
Also, you can get non-integer division by alternating or cycling the reload
value between two different numbers, e.g. using 11930 on one cycle then 11931
on the next to get 11930.5 (long term, that is :-)
One more thing - are you maintaining the BIOS timer tick interrupt? It is
supposed to be called every 65536 clocks. You can use a 16-bit scheduler
variable, and on every 10ms interrupt, add 11930 (or whatever you used) to the
scheduler variable, and when the add causes a carry, 65536 clocks have elapsed
so you should chain to the old int 8 handler rather than sending an EOI and
returning.
## 11.2 TIMER INTERRUPTS (INT 8, INT 1CH, RTC INTERRUPT)
----------
> If there are no TSRs hooking into it, what does the timer-tick interrupt do
> other than being used for counting the number of ticks since midnight?
The traditional functions of int 8 (the hardware timer tick interrupt) are (a)
updating the BIOS tick count variable which is used by DOS to determine the
time of day, and setting the midnight flag if a midnight has passed, and (b)
turning off the floppy drives after about two seconds since the last access.
BIOSes _may_ use int 8 for anything else that they like. For example they
_could_ use int 8 for green functions (e.g. spinning down the hard drive if
it has not been accessed for a while on a laptop or killing the video drive
if no video accesses have been made). I am not saying that BIOSes _do_ this,
just that it is their perogative to do this, so it's not safe to assume that
int 8 is only used to update the tick count and turn off the floppy drives.
Also, any number of TSRs and device drivers, such as screen savers and disk
caches, could be using int 8 and/or int 1Ch.
----------
> I have seen TSRs that hook int 1Ch rather than int 8, this implies that an
> application program should chain to the previous handler if it uses int
> 1Ch unless it has a good reason not to do so.
My understanding is that int 1Ch is intended for use by user programs only, and
that it should be neither necessary, nor desirable, to chain to the original
handler, as the original handler is just an IRET. The user program's only
obligation should be to restore the vector when it terminates. However, some
TSR writers obviously didn't think this way (or maybe just didn't _think_ :-)
so there are TSRs that hook int 1Ch. For their benefit your application can
and should chain int 1Ch. But I do not believe TSRs should use int 1Ch.
----------
> What are the advantages of using int 8 versus int 1Ch? Documents I've read
> recommend using int 1Ch. Why would you use int 8 instead?
It depends what you want to do with the interrupt. If you just want a 54.9254
ms regular interrupt in an application program (i.e. not a TSR), you can use
either.
If you are writing a TSR, you should use int 8, not int 1Ch, because int 1Ch is
intended for use by user programs, and a TSR is not a user program, it is more
like an operating system extension, and a user program is within its rights to
come along and hook int 1Ch without chaining to your handler. In this case
(using int 8 in a TSR), you must chain to the original handler. The simplest
way is just to JMP to it at the end of your intercepting code.
If you are modifying the timer tick rate, or doing vertical retrace emulation,
or anything clever with the timer, you must use int 8, and ensure that the old
int 8 handler is chained at appropriate intervals. This technique cannot safely
be used inside a TSR because an application is at liberty to pull the same
tricks and break the TSR.
In all cases, keep the amount of time spent in the interrupt handler to a
minimum.
----------
> How can I increment a variable once every second, under interrupt?
Timed interrupts on the PC can be generated via channel 0 of the timer chip
(8253 or 8254) and via the real time clock (RTC).
The timer cannot generate interrupts at one second intervals. It is normally
operating at 18.2065 interrupts per second (this is called the 'timer tick').
You can hook into this timer tick interrupt (int 8 or int 1Ch if you're writing
an application, int 8 only if you're writing a TSR). You can then count off
interrupts and increment your seconds counter every 18.2065 interrupts. This
is done by incrementing it after 18 or 19 interrupts, and alternating between
18 interrupts between increments, and 19 interrupts between increments, to give
over the medium term or long term, one increment every 18.2065 interrupts.
This requires some simple arithmetic. Of course this will cause the seconds
variable to be incremented slightly unevenly. If that's acceptable, this is
probably the best way to go. This technique can be used in an application or
a TSR.
If a slight unevenness in timing is not acceptable, you can reprogram timer
channel 0 to operate at a different rate, such as, say, 50 ticks per second,
and hook int 8, and call the old int 8 handler ('chain') 18.2065 times per
second. The timer cannot generate exactly 50 interrupts per second with a
single divisor value, but this can be achieved by dynamically reloading the
timer divisor on each interrupt. Of course this method makes the calls to
the old int 8 handler uneven, but this is not a problem for the software that
uses this interrupt. You then can count off 50 fast interrupts and increment
your seconds variable. However, this technique cannot safely be used in a TSR.
The above techniques use the timer (8253/8254). If you know your program will
always run on an AT or later, you can use the RTC. It is able to generate an
interrupt every second, but this mode is not normally used. I've never tried
using the update interrupt (once per second) but it should work, provided that
you use the normal tricks to make sure the BIOS doesn't turn off the interrupt
source. Alternatively, you could use the RTC at 1024 interrupts per second and
count off the interrupts yourself. This technique will definitely work, though
you are more likely to miss interrupts because they are happening at a faster
rate.
----------
> What is the difference, and interaction, between the timer tick interrupt
> and the real time clock's periodic interrupt?
The timer interrupt is triggered by channel 0 of the timer chip, an Intel 8253
or 8254 or workalike. It is normally operated at 18.2065 interrupts per second
(this is called the timer tick rate). The default handler is responsible for
maintaining the system time (which is done through the BIOS Tick Count Variable
in the BIOS Data Area) and turning off the floppy drive motors after two seconds
of inactivity, and it is likely that some machines use it for other purposes
too. The timer tick interrupt is IRQ0, which is int 8. This is the highest
priority hardware interrupt request. As well as updating the time and turning
off the floppy drive motors, the default handler issues int 1Ch on every tick.
This interrupt is intended for use by user programs (not TSRs) as a regular
interrupt source. TSRs and network software often intercept int 8 and use it
for timing, timeout detection, regular updating, etc etc. The timer interrupt
can be operated at a higher rate if desired (this technique cannot be used in a
TSR). It can be programmed to occur at 1.19318166666...MHz divided by any
integer from 2 to 65536 (very small divisors cause major overhead problems!).
With trickery (the dynamic timer tick technique) it can be made to occur at a
convenient rate, e.g. 1000 interrupts per second, etc. The program operating
the timer at a higher rate must chain to the original handler at the correct
rate, i.e. 18.2065 times per second.
The timer and int 8 are present in all PC-compatible machines.
The RTC is only present in the AT and later machines, which these days is at
least 99% of the market. It is connected to IRQ8, which is int 70h. This is
the highest priority interrupt on the slave interrupt controller, so it is
third highest priority on the machine (highest and second highest are int 8,
the timer tick, and int 9, the keyboard scancode interrupt). IRQ8 interrupt
is generated by the RTC (Real Time Clock) chip, which also holds the machine's
CMOS memory for storing BIOS settings. The interrupt can be programmed to
occur at a particular time (through the Alarm function of the RTC), every
second (the 'update' interrupt), or at one of the following rates (the periodic
interrupt) - 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, or 8192
interrupts per second. The RTC interrupt is used by several BIOS functions,
and the BIOS interrupt handler will sometimes turn off the interrupt, so when
you hook this interrupt, you have to chain to the BIOS's handler then turn the
interrupt source back on, just in case.
## 11.3 INTERRUPT PRIORITIES AND NESTING
----------
> While an interrupt handler is in progress, if the interrupt flag is cleared,
> can the handler be interrupted by another hardware interrupt? Also, if an
> interrupt handler takes a long time to run, can it be interrupted by itself
> (for example, a keyboard interrupt handler)?
No hardware interrupt will be accepted if the interrupt flag is clear, as it
is on entry to an interrupt handler. But, it is normal for most interrupt
handlers to enable interrupts via an STI instruction fairly early on, so that
higher priority interrupts will be able to interrupt them.
Hardware interrupts are prioritised by the 8259 interrupt controller(s).
Lower IRQ numbers are higher priority (unless software has reprogrammed the
interrupt controller modes). IRQ8-15 (not present on original PC and XT) fit
in between IRQ1 and IRQ3. In other words, the priority order is:
IRQ0 INT 8 Timer tick interrupt (HIGHEST PRIORITY)
IRQ1 INT 9 Keyboard scancode interrupt
IRQ2 INT 0Ah Uncommitted (see IRQ9) (ONLY PRESENT ON ORIGINAL PC AND XT)
IRQ8 INT 70h RTC interrupt
IRQ9 INT 71h Redirected IRQ2, uncommitted (COM ports, vertical retrace)
IRQ10 INT 72h Unallocated
IRQ11 INT 73h Unallocated
IRQ12 INT 74h Bus mouse hardware interrupt
IRQ13 INT 75h Math coprocessor
IRQ14 INT 76h Hard disk (AT and later)
IRQ15 INT 77h Unallocated
IRQ3 INT 0Bh COM2, usually, or uncommitted
IRQ4 INT 0Ch COM1, usually
IRQ5 INT 0Dh Uncommitted (COM ports, sound cards, XT hard disk)
IRQ6 INT 0Eh Floppy disk hardware interrupt
IRQ7 INT 0Fh Parallel port, sound cards (LOWEST PRIORITY)
If an interrupt handler is in progress, say on IRQ4 for example, and the handler
enables interrupts using STI, then a higher priority interrupt, such as the
keyboard scancode interrupt or timer tick interrupt, if signalled, _will_
interrupt the interrupt handler in progress. Once that higher priority
interrupt has been processed, the lower priority interrupt handler will be
resumed. But, a lower or equal priority interrupt will _not_ interrupt the
handler in progress, until that handler has sent an EOI (end of interrupt)
command to the interrupt controller(s) (first interrupt controller for IRQ0-7,
both interrupt controllers for IRQ8-15). The EOI command is value 20h, and is
sent to I/O port 20h for the first interrupt controller, and port 0A0h for the
second interrupt controller. The EOI command tells the interrupt controller
that the interrupt that was signalled by the interrupt controller (which
provides the interrupt vector to tell the processor where the interrupt handler
begins) is now finished with, and it resets the interrupt controller's logic.
The interrupt controller will then signal any other interrupts that were
pending. For example if an IRQ7 came along while IRQ4 was being processed,
the interrupt controller would ignore it until the IRQ4 handler issued an EOI,
then the interrupt controller would re-evaluate its pending interrupts and
issue the highest priority pending interrupt, which would be IRQ7.
To avoid the lower priority interrupt 'nesting' on top of the higher priority
interrupt and causing stack growth, the EOI command is normally issued right
at the end of the interrupt handler, and is issued with interrupts locked out,
so that the interrupt handler will return to the main code before the lower
priority interrupt is accepted.
If you specifically want a particular interrupt priority to be able to interrupt
itself, this is normally possible - just send the EOI at an early stage in the
interrupt handler, and make sure interrupts are enabled. The interrupt
controller thinks the interrupt handler has finished, so it will signal the
interrupt again if the interrupt is triggered again during processing of the
same priority interrupt. Of course this also lets lower priority interrupts
through as well.
--------
> The timer triggers IRQ0 (int 8) which has highest priority. Does this mean
> that it really interrupts another lower priority interrupt, or does it only
> mean that if there are several interrupts pending, IRQ0 will be chosen?
First, no IRQ will interrupt _anything_ if the interrupt flag in the processor
is clear (via CLI). This flag is also cleared automatically on entry to any
interrupt handler, and must be explicitly set by the interrupt handler. Most
software and hardware interrupt handlers will do this, unless they have some
special reason for not doing so.
If a lower priority interrupt is in progress, and the interrupt flag is set
(interrupts are enabled), then a higher priority interrupt _will_ interrupt
that interrupt handler. When the higher priority interrupt exits, the lower
priority interrupt handler is resumed, in the normal way. If an interrupt of
the same or lower priority occurs, it will not be serviced until the current
interrupt handler has finished and sent an end of interrupt signal (more
below).
If interrupts are locked out via the interrupt flag in the processor, the
interrupt controller chip will continually evaluate its inputs, keeping track
of the highest priority pending input, and when the processor is able to accept
the interrupt, the interrupt controller will first issue the highest priority
interrupt.
If a hardware interrupt request disappears while the interrupt controller is
waiting for the processor to acknowledge its interrupt request (INTR), the
interrupt controller is in the embarrassing position of having interrupted the
processor but not having a valid interrupt request to issue. In this case,
the interrupt controller issues an interrupt level 7. If the fleeting
interrupt was on the primary interrupt controller (IRQ0, IRQ1, or IRQ3-7),
this will cause IRQ7 (int 0Fh) to be executed. If the fleeting interrupt was
on the secondary interrupt controller (IRQ8-15), this will cause IRQ15 (int
77h) to be executed. Any program handling IRQ7 and/or IRQ15 should be
prepared for this possibility.
The interrupt controller keeps track of the current interrupt priority. It
knows when the interrupt priority changes to a higher priority, because it
issued the interrupt request itself. It also knows when the higher priority
interrupt ends, and a lower priority interrupt resumes, via the end of
interrrupt command.
> What is an EOI (end of interrupt) and what type should I use?
Any hardware interrupt handler must notify the interrupt controller (or
controllers, if it's IRQ8 or higher) when it has completed, so that the
interrupt controller can keep track of interrupt levels in progress, etc.
It does this partly through the EOI (end of interrupt) command. There are
two types of EOI - the non-specific EOI and the specific EOI. Specific EOI
is not often used, though any 8259 compatible interrupt controller should
support it. It simply tells the interrupt controller that a specific interrupt
handler has finished. The non-specific EOI tells the interrupt controller that
the currently executing, highest priority interrupt handler has finished. The
command is sent like this. The interrupt controller knows the highest priority
executing interrupt level, because it generated the interrupt request and
provided the vector.
Assembler mov al,20h
out 20h,al
Micro$oft C outp(0x20, 0x20);
Borland C outportb(0x20, 0x20);
Pascal port[$20]=$20;
GW-BASIC Just kidding :-)
If the interrupt handler is for IRQ8 or higher, it must send an EOI command
(0x20) to the secondary interrupt controller, at I/O address 0xA0, as well.
I don't believe it really matters in what order these EOIs are sent in this
case.
If you are hooking int 8, then you should chain to the original int 8 handler,
unless you have a special reason for not doing so. The original int 8 handler
is part of the BIOS. It will send the EOI for you.
----------
> How do I tell if my timer tick interrupt handler is taking too much time, and
> what would happen if the interrupt was to get called again while the handler
> was still running from the first time?
Until you send the EOI or chain to the original handler (in the case of int 8)
or until you return (in the case of int 1Ch), the interrupt will not be called
again while it is still running.
> How are IRQ2 and IRQ9 related? I have run out of free IRQs except for IRQ2,
> which I have kept free, since I have IRQ9 in use, and wanted to avoid any
> problem. Can I use IRQ2?
If you have IRQ9 in use, you are going to have trouble 'using' IRQ2 because the
slot bus pin that was IRQ2 on the PC and XT is IRQ9 on later machines, so IRQ2
doesn't 'exist' any more :-)
IRQ2 was just a standard interrupt on the PC and XT, with no assigned purpose
(often used for extra COM ports or special hardware boards). The AT added a
second interrupt controller (Intel 8259) which provides IRQ8 through IRQ15
inputs, but required a 'cascade' interrupt input into the main interrupt
controller, and IRQ2 was chosen as the cascade interrupt. The slot bus pin
that used to carry IRQ2 was fed into IRQ9, on the second interrupt controller,
and BIOS and DOS were modified to software-redirect IRQ9 to IRQ2 so that many
programs that were able to use IRQ2 would still work properly and be none the
wiser on ATs when they would really be using IRQ9. The default IRQ9 handler
sends the EOI to the secondary interrupt controller, then invokes the IRQ2
handler through the IRQ2 vector. When the IRQ2 handler sends its EOI to the
primary interrupt controller, the IRQ9 is fully acknowledged.
So that is the relationship between the two interrupts. IRQ2 is not accessible
on the slot bus on ATs and later machines. This may not apply to MicroChannel
motherboards, BTW, which were designed after the AT.
## 11.4 INTERRUPT HANDLER RESTRICTIONS
----------
> I'm writing a TSR that will make my computer beep several times when "RING"
> is received from my modem. I want to make it a hook int 1Ch and make it
> watch for "RING" using the BIOS serial functions interrupt 14h function 3,
> then activate the beep. Is it safe to call int 14h from within an int 1Ch
> handler?
First, TSRs shouldn't use int 1Ch, use int 8 instead, and make sure you chain
to the original handler. BIOS functions are nominally non-reentrant, but the
int 14h services are so simple that provided the foreground program isn't using
them to access the same serial port (very unlikely, as any decent comms software
goes direct to hardware and doesn't use int 14h at all), you should be safe.
But if you're using int 14h and calling it from within your int 8 handler, you
won't call it quickly enough to catch the 'RING' string from the modem - you
will get a receive overrun, unless you have an internal modem with an emulated
serial port, which will hold the data until it is read. IOW, you should hook
the serial interrupt for the serial port you're monitoring, and enable the
serial interrupt, etc, so you get an interrupt when the modem sends something.
Finally, how are you planning to program the beep? I suggest going direct to
hardware, rather than using int 10h, which can often be non-reentrant.
That means you must turn the sound on and off using the timer interrupt.
----------
> When I add a call to puts() in my timer interrupt handler, the machine
> locks up or crashes with an EMM386 or QEMM exception. Why?
Because puts() calls DOS and DOS is non-reentrant. When the timer tick
interrupt is signalled, various parts of your computer's software and hardware
may be 'busy', and calling most DOS functions and some BIOS functions will
usually cause problems with reentrancy. If you want to output to the screen
from within an interrupt handler, you either need to use TSR techniques to
ensure that DOS or the BIOS is not busy, or write directly to screen memory.
I find the latter technique more useful.
--------
> Can I save to disk some data which I collect in my interrupt handler?
Yes, absolutely. Especially if it's not a TSR. You can't write to disk from
your int 8 handler, though - the BIOS might be in the middle of writing
something else. There are lots of reasons why this would be very dangerous.
Normally this would be done using a circular buffer, or 'queue', to pass data
from your interrupt handler to your mainline. You have an area of memory (any
size from a few bytes up to 32K or so) to be used circularly to store data, and
have two pointers or offsets into the buffer, one being driven by the interrupt
routine showing where the data is going in, and one controlled by the mainline
which runs 'in the foreground' keeping track of data coming out of the circular
buffer and being written to disk. Every time your interrupt routine puts data
in the buffer, it 'bumps' the 'ingoing' pointer (increment pointer, check
whether it has gone off the end of the buffer, and if so, reset it to the start
of the buffer). Every time your mainline gets data out of the buffer, it bumps
its outgoing pointer in the same way. If the two pointers are equal, there is
no new data in the buffer. The interrupt handler should also handle a buffer
overrun tidily, by checking for the 'ingoing' pointer crossing the 'outgoing'
pointer and behaving accordingly (e.g. don't update the 'ingoing' pointer, and
set a global variable somewhere that the mainline can detect, that indicates a
buffer overflow). The mainline would put the data from the circular buffer in
to a linear buffer and write that buffer to disk using the standard file I/O
routines or DOS services when it gets full.
## 11.5 HIGH SPEED TIMER TICK
----------
> I need to trigger an analog to digital converter (which does not have its own
> clock) 4000 times per second.
This can be done by speeding up the timer tick, see section »» 8 and
subsections. To get exactly 4000 interrupts per second, you need to use
the dynamic tick period technique described in section »» 8.6.
----------
> I have a 8kbps data stream that I want to capture. I need the computer to
> synchronise to the data stream. Could I do this in software?
Timer channel 0 can be made to run at 8000 samples per second, but the internal
timing sources are difficult to synchronise to an external signal. It might
be possible, but I'd first suggest an external PLL synchronised with the
signal, triggering interrupts via the ACK pin on a parallel port or through
a flow control line on a serial port.
## 11.6 DOS DATE AND TIME
----------
> Where and how does DOS store the date?
DOS stores the date as a number of days since 1/1/1980 internally in the CLOCK$
device driver, which is part of IO.SYS (MS-DOS) or IBMBIO.COM (other DOSes).
There does not seem to be any way to locate the variable except manually,
using a debugger.
----------
> I am using the RTC to keep the DOS clock in line. Just after midnight, the
> date counts back a day. If I don't set the DOS clock there is no problem.
> There is a byte in the BIOS Data Area at 0040:0070 which tells the system
> that the date rolled over. How does this work?
The midnight flag at 0040:0070 is set to 1 (or just incremented by some BIOSes)
by the BIOS's int 8 (timer tick) handler when the tick count rolls over from
0x001800AF to 0x00000000 (i.e. at midnight).
Every time the BIOS request-tick-count function (int 1Ah with AH=00) is called,
this flag is returned in AL, and the flag byte in memory is cleared. The flag
byte is also cleared if the set-tick-count function (int 1Ah with AH=01) is
called.
DOS relies on this flag when it calls the BIOS function. If your program is
using the BIOS request-tick-count function, your program will be notified of
the change of day, but DOS will not, because the flag is cleared as soon as it
is reported - the BIOS doesn't care whether your program, or DOS, called the
function, so DOS misses out on seeing the flag, and doesn't increment the date.
In other words, don't use int 1Ah functions 00 and 01, and the DOS date will
update properly. If you want to read or write the tick count, access it
directly at 0040:006C.
## 11.7 ACCESSING HARDWARE
----------
> How can I read current time without using any BIOS and DOS function calls?
You can access the RTC chip directly. The RTC is not present in the original
PC and XT and may not be present in non-hardware-compatible machines. The RTC
also implements the CMOS which stores your BIOS parameter settings, so be
careful when accessing it! See section »» 7.35. This gives a resolution of
one second. Also, you can read the BIOS Tick Count variable, but this is not
in convenient units. See section »» 4.
----------
> I have an acquisition card which measures voltage and frequency of
> electrical signals. This board can be configured to use IRQ2 through
> IRQ7 (jumper-selectable) and I/O address 300h. How can I access the
> devices via the I/O space?
The I/O space is accessed via the IN and OUT instructions of the CPU (if you're
writing in assembly). In C, use inportb() and outportb() (Borland) or inp() and
outp() (Micro$oft). In Turbo Pascal, use port[]. The x86 processor in the PC
can address up to 64K of I/O but the PC's I/O space is usually limited to the
range 0000h - 03FFh because ISA bus I/O cards only decode the bottom 10 bits of
the address on I/O accesses.
Your card will probably have a CPU-addressable device such as an 8255 (parallel
I/O chip) or similar, that will provide the interface between the hardware on
the board, and the software that you write. If you can identify this chip
(look for the biggest one, usually :-) and get the data sheet on it, you can
find out how to talk to it. If it's a proprietary ASIC, though, you might be
on your own. You could try asking the manufacturer nicely. If that fails,
you could try disassembling any software that came with the card and working
out what the I/O accesses are doing.
Typically cards like that will occupy 4, 8, 16, or sometimes 32 adjacent I/O
locations, and AFAIK the I/O space from 0300h to 031Fh is not normally used by
any standard PC peripheral, so you should be safe putting it there.
> And can I write an interrupt service routine that will perform I/O through
> port 300h? Is this a normal procedure?
Yes, and yes. The XT bus supports IRQ2-7. IRQ4 and IRQ3 are usually used for
COM1 and COM2 respectively, IRQ6 is usually used for the floppy disk, and IRQ7
is sometimes used for the first parallel port, and for sound cards. IRQ5 is
also sometimes used by sound cards, and is also often used by the hard drive on
XTs. Assuming you want to put your XT bus card into an AT, you should be able
to use IRQ2 or IRQ5 with it, without causing any conflicts. IRQ2 is actually
remapped to IRQ9 on ATs (long story).
There are several parts involved in setting up an interrupt handler, and I'd
suggest you first try just talking to the chips on the board, and do the
interrupt stuff later, as it can get a bit messy.
----------
> I need to delay program execution for 1ms. I found some old assembly code
> that used timer 2 on the PIT. It sets the timer to square wave mode, then
> counts state changes. This code doesn't work on a Pentium, 486, or 386 PC.
> How can I make this work on newer PCs? It appears that timer 2 output isn't
> tied to bit 5 of port 0x62 on these machines. Is there something different
> about how timer 2 and/or the speaker is implemented on newer PCs?
Yes. The ye olde machines used an 8255 at 60h-63h and the Timer 2 read-back
signal was on port C at 62h. The AT and later machines use a micro as the
keyboard interface, and don't implement any port at 62h at all (AFAIK). On
these machines, Timer 2 readback is on bit 5 of port 61h and operates in the
same way.
For your purposes, the Refresh Detect signal might be more appropriate. This
is a read-only signal on bit 4 of the port at 61h, on all machines except the
old PC and XT, though I wouldn't guarantee it's present on all IBM machines
(they seem to be the least compatible, for some stupid reason). Anyway, this
bit toggles state once every 15.0857 microseconds (or 216/14.31818, to be
exact). This can easily be read in a loop, and you can get a fairly accurate
delay using this method. It won't work properly if the DRAM refresh rate has
been changed, but people don't do that much any more :-) This method has
advantages over using Timer 2 because you can use it with interrupts enabled
and not have to worry about a keyboard buffer full beep clobbering your timer,
though of course any interrupts that are serviced during the delay will
lengthen the delay.
## 11.8 MISCELLANEOUS
----------
> How do I check for a keypress with a timeout?
You need to check for a keypress in a loop, and also incorporate a timeout check
in the loop. See the sample program in section »» 4.7 and the function in
section »» 4.8. You can't use getch() or any stream I/O functions, because
they will wait indefinitely for keys to be pressed. If your compiler supports
bioskey(1), you can use that, otherwise you can write a function that uses int
16h function 1, 11h, or 21h, or int 21h function 6, to poll for a keypress.
----------
> How do you create a clock that will run in the top right hand corner of
> the screen and let the user regain control of the computer in DOS?
> I think you have to 'hook' an interrupt to accomplish this.
Hook int 8, the timer interrupt (int 1Ch can also be used but is intended
for use by applications, which may not chain to the old handler, so your
TSR would stop updating while some apps are active). On every interrupt,
check the time, either from the BIOS tick count or from the real time clock.
You can redraw your time on the screen on every int 8 (i.e. 18.2065 times
per second), or just when the second changes, whichever you prefer. There
is a sample program in assembler that does this, in section »» 7.35.8.
----------
> In my Borland C++ program I need a delay of exactly 100 ms. How can I do it?
There are many ways to implement processor-independent delays on PCs. There
may be a problem depending on how exact you need your delay to be. There are
two approaches - wait in a loop for the appropriate length of time, or trigger
an interrupt at the correct time. With the first approach, you should leave
interrupts enabled during the loop (otherwise the machine will lose time, as
the timer tick interrupt comes along every 54.9254 ms), so any interrupts that
come in during the loop, or near the end of it, may cause your delay to be
longer than you expected. If you use the other method, acceptance of the
interrupt can be delayed by foreground code disabling interrupts, or by other
interrupts occuring during the delay. So there is no ideal solution if you
need a very accurate delay. If you can tolerate an error of, say, 1% to 5%,
and your program just wants to wait without doing anything else, and the
program will not need to run on old PC and XT machines, you can use the Refresh
Detect delay method, which is pretty tidy. If you can tolerate a resolution of
1ms, you can use the BIOS delay functions (not supported on old PC and XT
machines either). Otherwise you can use the interrupt method, which is fairly
tricky to program, or a method based on timer 2 (normally used for making beeps)
which has disadvantages too.
## 12 REFERENCES
These references are mostly from {JAM} John Mertus's article. He has given me
permission to include them. I have included comments on the subject matter of
the books where appropriate. They are in author order. There is no guarantee
that the books are still available, or that these are the latest editions.
Title: Assembly Language Programming for the IBM Personal Computer
Author: David J. Bradley
Published: Prentice-Hall, 1984
Comments: Possibly the first book to describe timing by reading the CTC
May be no longer available
Title: Interrupt List
Author: Ralf Brown
Comments: Electronic document available on Internet SimTel mirrors, e.g.
Oak as ftp://oak.oakland.edu/SimTel/msdos/info/inter*.zip.
Contains an exhaustive list of interrupt usage (mainly
software interrupts), DOS data structures, etc, essential!
Title: DOS Programmer's Reference, 3rd Edition
Author: Terry Dettmann, Jim Kyle, Marcus Johnson
Published: Que Corporation, 1992
ISBN: 0-88022-790-7; Library of Congress Catalog No. 91-66203
Title: EGA/VGA - A Programmer's Reference Guide
Author: Bradley Dyck Kliewer
Published: Intertext Publications / Multiscience Press, Inc, McGraw-Hill
Book Company, 11 West 19th Street, New York, NY 10011, 1988
ISBN: 0-07-035089-2
Comments: Good as a reference but not recommended for beginners
Title: IBM Personal System/2 Hardware Interface Technical Reference
Published: IBM Corporation, Boca Raton, Florida, 1990
Title: Technical Reference, Personal Computer XT
Published: IBM Corporation, Boca Raton, Florida, April 1983
Title: Peripheral Components Data Book
Published: Intel Corporation, Mt. Prospect, Illinois, 1994
Comments: Includes full data sheet on 8254, recommended.
According to information in the data book, it can be ordered
within USA from Literature Sales, P.O. Box 7641, Mt. Prospect,
IL 60056-7641 or by phone from USA and Canada on (800) 548-4725
voice or (708) 296-3699 fax, Intel order number 296467,
ISBN 1-55512-207-8. They accept credit cards, but you may
need to complete an order form.
Title: PC Programmer's Guide to Low-Level Functions and Interrupts
Author: Marcus Johnson
Published: Sams Publishing, 201 West 103rd Street, Indianapolis, Indiana,
46290, 1994.
Comments: Lots of useful low-level technical info, plus documentation on
BIOS, DOS, EMS, XMS, DPMI, and other APIs. Disk included.
Title: Accurate Timing under Microsoft Windows without
reprogramming the System Timer
Author: Jerry Jongerius
Published: Microsoft System Journal, 1991
Comments: Reading the CTC
Title: The MS-DOS Encyclopedia
Published: Microsoft Press, 16011 NE 36th Way, Box 97017, Redmond,
Washington 98073-9717, 1988
ISBN: 1-55615-174-8
Comments: Includes sections on interrupt-driven communications, TSR
programming, exception handlers, hardware interrupt handlers,
and debugging, DOS and BIOS function reference, and usage for
DOS utilities, highly recommended. [KH]
Title: The Peter Norton Programmer's Guide to the IBM PC
Author: Peter Norton
Published: Microsoft Press, 16011 NE 36th Way, Box 97017, Redmond,
Washington 98073-9717, 1985
ISBN: 0-914845-46-2, Penguin ISBN 0-14-087-144-6
Comments: There is a newer edition
Title: The Winn Rosch Hardware Bible
Author: Winn L. Rosch
Published: Brady Books, New York, 1989
Title: The IBM Personal Computer from the Inside Out
Author: Murry Sargent and Richard L. Shoemaker
Published: Addison-Wesley Publishing Co, Reading, Massachusetts, 1986
Title: Netware, the Professional Reference (second edition)
Author: Karanjit Siyan
Published: New Rider Publishers, Carmel, Indiana, 1993
Title: The Waite Group's MS-DOS Developer's Guide (second edition)
Published: Howard W. Sams & Company, 4300 West 62nd Street, Indianapolis,
Indiana 46268, 1989
ISBN: 0-672-22630-8 (Library of Congress Catalog Card 88-62227)
Comments: Includes info on TSRs, serial port, EGA and VGA, and real-time
programming.
Title: Programmer's Guide to PC & PS/2 Video Systems
Author: Richard Wilton
Published: Microsoft Press, One Microsoft Way, Redmond, Washington
98052-6399, 1987
ISBN: 1-55615-103-9
Comments: Very readable book, recommended [KH]
End of the PC Timing FAQ / Application notes
Please drop me a line if you find this document
useful, or if you have anything to add.
----//----